Compare commits

..

4 Commits

Author SHA1 Message Date
Asim Aslam 6d21eed2a5 Update quic-go for checksum mismatch issues 2019-10-12 12:20:02 +01:00
Milos Gajdos e8e5871521 heck cache status error
(cherry picked from commit fc89c9831e)
2019-10-12 12:07:11 +01:00
Asim Aslam c8d71e051a Fix race in cache 2019-10-07 09:09:39 +01:00
Asim Aslam 6734f3800b fix major deadlock in registry cache 2019-10-07 09:09:31 +01:00
744 changed files with 34533 additions and 53854 deletions
-1
View File
@@ -1 +0,0 @@
github: asim
-44
View File
@@ -1,44 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: '[BUG] '
labels: bug
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce
Steps to reproduce the behavior:
1. Create service with '...'
2. Configure plugin '...'
3. Run command '...'
4. See error
## Expected behavior
A clear and concise description of what you expected to happen.
## Code sample
```go
// Minimal reproducible code
```
## Environment
- Go Micro version: [e.g. v5.3.0]
- Go version: [e.g. 1.21.0]
- OS: [e.g. Ubuntu 22.04]
- Plugins used: [e.g. consul registry, nats broker]
## Logs
```
Paste relevant logs here
```
## Additional context
Add any other context about the problem here.
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Examples](https://github.com/micro/go-micro/tree/master/internal/website/docs/examples)
- [API Reference](https://pkg.go.dev/go-micro.dev/v5)
-30
View File
@@ -1,30 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[FEATURE] '
labels: enhancement
assignees: ''
---
## Is your feature request related to a problem?
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
## Describe the solution you'd like
A clear and concise description of what you want to happen.
## Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
## Use case
Describe how this feature would be used in practice. What problem does it solve?
## Additional context
Add any other context, code examples, or screenshots about the feature request here.
## Willing to contribute?
- [ ] I'd be willing to submit a PR for this feature
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Plugins](https://github.com/micro/go-micro/tree/master/internal/website/docs/plugins.md)
- [Roadmap](https://github.com/micro/go-micro/blob/master/ROADMAP.md)
-31
View File
@@ -1,31 +0,0 @@
---
name: Question
about: Ask a question about using Go Micro
title: '[QUESTION] '
labels: question
assignees: ''
---
## Your question
A clear and concise question about Go Micro usage.
## What have you tried?
Describe what you've already attempted or researched.
## Code sample (if applicable)
```go
// Your code here
```
## Context
Provide any additional context that might help answer your question.
## Resources you've checked
- [ ] [Getting Started Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/getting-started.md)
- [ ] [Examples](https://github.com/micro/go-micro/tree/master/internal/website/docs/examples)
- [ ] [API Documentation](https://pkg.go.dev/go-micro.dev/v5)
- [ ] Searched existing issues
## Helpful links
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Plugins Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/plugins.md)
-74
View File
@@ -1,74 +0,0 @@
name: Run Tests
on:
push:
branches:
- "**"
pull_request:
types:
- opened
- reopened
branches:
- "**"
jobs:
unittests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.24
check-latest: true
cache: true
- name: Get dependencies
run: |
go install github.com/kyoh86/richgo@latest
go get -v -t -d ./...
- name: Run tests
id: tests
run: richgo test -v -race -cover ./...
env:
IN_TRAVIS_CI: yes
RICHGO_FORCE_COLOR: 1
etcd-integration:
name: Etcd Integration Tests
runs-on: ubuntu-latest
permissions:
contents: read
services:
etcd:
image: quay.io/coreos/etcd:v3.5.2
env:
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379
ports:
- 2379:2379
options: >-
--health-cmd "etcdctl endpoint health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.24
check-latest: true
cache: true
- name: Get dependencies
run: |
go install github.com/kyoh86/richgo@latest
go get -v -t -d ./...
- name: Wait for etcd
run: |
timeout 30 bash -c 'until curl -s http://localhost:2379/health; do sleep 1; done'
- name: Run etcd integration tests
run: richgo test -v -race ./registry/etcd/...
env:
ETCD_ADDRESS: localhost:2379
IN_TRAVIS_CI: yes
RICHGO_FORCE_COLOR: 1
-51
View File
@@ -1,51 +0,0 @@
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll with GitHub Pages dependencies preinstalled
on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./internal/website
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-5
View File
@@ -1,7 +1,6 @@
# Develop tools
/.vscode/
/.idea/
/.trunk
# Binaries for programs and plugins
*.exe
@@ -35,7 +34,3 @@ _cgo_export.*
*~
*.swp
*.swo
# go work files
go.work
go.work.sum
-251
View File
@@ -1,251 +0,0 @@
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# go: '1.18'
# default concurrency is a available CPU number
# concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 10m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
[]
# - .*\\.pb\\.go$
allow-parallel-runners: true
# list of build tags, all linters use it. Default is empty list.
build-tags: []
# output configuration options
output:
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
#
# Multiple can be specified by separating them by comma, output can be provided
# for each of them by separating format name and path by colon symbol.
# Output path can be either `stdout`, `stderr` or path to the file to write to.
# Example: "checkstyle:report.json,colored-line-number"
#
# Default: colored-line-number
format: colored-line-number
# Print lines of code with issue.
# Default: true
print-issued-lines: true
# Print linter name in the end of issue text.
# Default: true
print-linter-name: true
# Make issues output unique by line.
# Default: true
uniq-by-line: true
# Add a prefix to the output file references.
# Default is no prefix.
path-prefix: ""
# Sort results by: filepath, line and column.
sort-results: true
# all available settings of specific linters
linters-settings:
wsl:
allow-cuddle-with-calls: ["Lock", "RLock", "defer"]
funlen:
lines: 80
statements: 60
varnamelen:
# The longest distance, in source lines, that is being considered a "small scope".
# Variables used in at most this many lines will be ignored.
# Default: 5
max-distance: 26
ignore-names:
- err
- id
- ch
- wg
- mu
ignore-decls:
- c echo.Context
- t testing.T
- f *foo.Bar
- e error
- i int
- const C
- T any
- m map[string]int
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: true
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: true
govet:
# report about shadowed variables
check-shadowing: false
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 15
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
depguard:
list-type: blacklist
# Packages listed here will reported as error if imported
packages:
- github.com/golang/protobuf/proto
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 120
# tab width in spaces. Default to 1.
tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
# and rta for programs with main packages. Default is cha.
algo: cha
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 60
nolintlint:
allow-unused: false
allow-leading-space: false
allow-no-explanation: []
require-explanation: false
require-specific: true
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
cyclop:
# the maximal code complexity to report
max-complexity: 20
gomoddirectives:
replace-local: true
retract-allow-no-explanation: false
exclude-forbidden: true
linters:
enable-all: true
disable-all: false
fast: false
disable:
- golint
- varcheck
- ifshort
- structcheck
- deadcode
# - nosnakecase
- interfacer
- maligned
- scopelint
- exhaustivestruct
- testpackage
- promlinter
- nonamedreturns
- makezero
- gofumpt
- nlreturn
- thelper
# Can be considered to be enabled
- gochecknoinits
- gochecknoglobals # RIP
- dogsled
- wrapcheck
- paralleltest
- ireturn
- gomnd
- goerr113
- exhaustruct
- containedctx
- godox
- forcetypeassert
- gci
- lll
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
# exclude:
# - package comment should be of the form "Package services ..." # revive
# - ^ST1000 # ST1000: at least one file in a package should have a package comment (stylecheck)
# exclude-rules:
# - path: internal/app/machined/pkg/system/services
# linters:
# - dupl
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- dupl
- gosec
- funlen
- varnamelen
- wsl
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: false
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
-29
View File
@@ -1,29 +0,0 @@
labelType: long
coverThreshold: 70
buildStyle:
bold: true
foreground: yellow
startStyle:
foreground: lightBlack
passStyle:
foreground: green
failStyle:
bold: true
foreground: "#821515"
skipStyle:
foreground: lightBlack
passPackageStyle:
foreground: green
hide: false
failPackageStyle:
bold: true
foreground: "#821515"
coveredStyle:
foreground: green
uncoveredStyle:
bold: true
foreground: yellow
fileStyle:
foreground: cyan
lineStyle:
foreground: magenta
+11
View File
@@ -0,0 +1,11 @@
language: go
go:
- 1.13.x
env:
- GO111MODULE=on
notifications:
slack:
secure: aEvhLbhujaGaKSrOokiG3//PaVHTIrc3fBpoRbCRqfZpyq6WREoapJJhF+tIpWWOwaC9GmChbD6aHo/jMUgwKXVyPSaNjiEL87YzUUpL8B2zslNp1rgfTg/LrzthOx3Q1TYwpaAl3to0fuHUVFX4yMeC2vuThq7WSXgMMxFCtbc=
cache:
directories:
- $GOPATH/pkg/mod
-207
View File
@@ -1,207 +0,0 @@
# Contributing to Go Micro
Thank you for your interest in contributing to Go Micro! This document provides guidelines and instructions for contributing.
## Code of Conduct
Be respectful, inclusive, and collaborative. We're all here to build great software together.
## Getting Started
1. Fork the repository
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/go-micro.git`
3. Add upstream remote: `git remote add upstream https://github.com/micro/go-micro.git`
4. Create a feature branch: `git checkout -b feature/my-feature`
## Development Setup
```bash
# Install dependencies
go mod download
# Run tests
go test ./...
# Run tests with coverage
go test -race -coverprofile=coverage.out ./...
# Run linter (install golangci-lint first)
golangci-lint run
```
## Making Changes
### Code Guidelines
- Follow standard Go conventions (use `gofmt`, `golint`)
- Write clear, descriptive commit messages
- Add tests for new functionality
- Update documentation for API changes
- Keep PRs focused - one feature/fix per PR
### Commit Messages
Use conventional commits format:
```
type(scope): subject
body
footer
```
Types:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `test`: Test additions/changes
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `chore`: Maintenance tasks
Examples:
```
feat(registry): add kubernetes registry plugin
fix(broker): resolve nats connection leak
docs(examples): add streaming example
```
### Testing
- Write unit tests for all new code
- Ensure existing tests pass
- Add integration tests for plugin implementations
- Test with multiple Go versions (1.20+)
```bash
# Run specific package tests
go test ./registry/...
# Run with verbose output
go test -v ./...
# Run specific test
go test -run TestMyFunction ./pkg/...
```
### Documentation
- Update relevant markdown files in `internal/website/docs/`
- Add examples to `internal/website/docs/examples/` for new features
- Update README.md for major features
- Add godoc comments for exported functions/types
## Pull Request Process
1. **Update your branch**
```bash
git fetch upstream
git rebase upstream/master
```
2. **Run tests and linting**
```bash
go test ./...
golangci-lint run
```
3. **Push to your fork**
```bash
git push origin feature/my-feature
```
4. **Create Pull Request**
- Use a descriptive title
- Reference any related issues
- Describe what changed and why
- Add screenshots for UI changes
- Mark as draft if work in progress
5. **PR Review**
- Respond to feedback promptly
- Make requested changes
- Re-request review after updates
### PR Checklist
- [ ] Tests pass locally
- [ ] Code follows Go conventions
- [ ] Documentation updated
- [ ] Commit messages are clear
- [ ] Branch is up to date with master
- [ ] No merge conflicts
## Adding Plugins
New plugins should:
1. Live in the appropriate interface directory (e.g., `registry/myplugin/`)
2. Implement the interface completely
3. Include comprehensive tests
4. Provide usage examples
5. Document configuration options (env vars, options)
6. Add to plugin documentation
Example structure:
```
registry/myplugin/
├── myplugin.go # Main implementation
├── myplugin_test.go # Tests
├── options.go # Plugin-specific options
└── README.md # Usage and configuration
```
## Reporting Issues
Before creating an issue:
1. Search existing issues
2. Check documentation
3. Try the latest version
When reporting bugs:
- Use the bug report template
- Include minimal reproduction code
- Specify versions (Go, Go Micro, plugins)
- Provide relevant logs
## Documentation Contributions
Documentation improvements are always welcome!
- Fix typos and grammar
- Improve clarity
- Add missing examples
- Update outdated information
Documentation lives in `internal/website/docs/`. Preview locally with Jekyll:
```bash
cd internal/website
bundle install
bundle exec jekyll serve --livereload
```
## Community
- GitHub Issues: Bug reports and feature requests
- GitHub Discussions: Questions, ideas, and community chat
- Sponsorship: [GitHub Sponsors](https://github.com/sponsors/micro)
## Release Process
Maintainers handle releases:
1. Update CHANGELOG.md
2. Tag release: `git tag -a v5.x.x -m "Release v5.x.x"`
3. Push tag: `git push origin v5.x.x`
4. GitHub Actions creates release
## Questions?
- Check [documentation](internal/website/docs/)
- Browse [examples](internal/website/docs/examples/)
- Open a [question issue](.github/ISSUE_TEMPLATE/question.md)
Thank you for contributing to Go Micro! 🎉
+2 -12
View File
@@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -175,18 +176,7 @@
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2015 Asim Aslam.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+31 -203
View File
@@ -1,221 +1,49 @@
# Go Micro [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go-micro.dev/v5?tab=doc) [![Go Report Card](https://goreportcard.com/badge/github.com/go-micro/go-micro)](https://goreportcard.com/report/github.com/go-micro/go-micro)
# Go Micro [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GoDoc](https://godoc.org/github.com/micro/go-micro?status.svg)](https://godoc.org/github.com/micro/go-micro) [![Travis CI](https://api.travis-ci.org/micro/go-micro.svg?branch=master)](https://travis-ci.org/micro/go-micro) [![Go Report Card](https://goreportcard.com/badge/micro/go-micro)](https://goreportcard.com/report/github.com/micro/go-micro)
Go Micro is a framework for distributed systems development.
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro)
Go Micro is a framework for microservice development.
## Overview
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
The Go Micro philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
but everything can be easily swapped out.
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
The **micro** philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
but everything can be easily swapped out.
<img src="https://micro.mu/docs/images/go-micro.svg" />
Plugins are available at [github.com/micro/go-plugins](https://github.com/micro/go-plugins).
Follow us on [Twitter](https://twitter.com/microhq) or join the [Slack](http://slack.micro.mu/) community.
## Features
Go Micro abstracts away the details of distributed systems. Here are the main features.
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
zero trust networking by providing every service an identity and certificates. This additionally includes rule
based access control.
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system. You can optionally set gossip using the SWIM protocol for p2p networks or consul for a
resilient cloud-native setup.
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
across the services and retry a different node if there's a problem.
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for many storage backends
in the plugins repo. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
and server handle this by default. This includes protobuf and json by default.
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system.
- **Request/Response** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed. The default
transport is http/1.1 or http2 when tls is enabled.
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
across the services and retry a different node if there's a problem.
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development. The default messaging is point-to-point http/1.1 or http2 when tls
is enabled.
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
and server handle this by default. This includes protobuf and json by default.
- **RPC Client/Server** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology.
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology. Find plugins in
[github.com/micro/go-plugins](https://github.com/micro/go-plugins).
## Getting Started
To make use of Go Micro
See the [docs](https://micro.mu/docs/go-micro.html) for detailed information on the architecture, installation and use of go-micro.
```bash
go get go-micro.dev/v5@latest
```
Create a service and register a handler
```go
package main
import (
"go-micro.dev/v5"
)
type Request struct {
Name string `json:"name"`
}
type Response struct {
Message string `json:"message"`
}
type Say struct{}
func (h *Say) Hello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
func main() {
// create the service
service := micro.New("helloworld")
// register handler
service.Handle(new(Say))
// run the service
service.Run()
}
```
Set a fixed address
```go
service := micro.NewService(
micro.Name("helloworld"),
micro.Address(":8080"),
)
```
Call it via curl
```bash
curl -XPOST \
-H 'Content-Type: application/json' \
-H 'Micro-Endpoint: Say.Hello' \
-d '{"name": "alice"}' \
http://localhost:8080
```
## Experimental
There's a new `genai` package for generative AI capabilities.
## Protobuf
Install the code generator and see usage in the docs:
```bash
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
```
Docs: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
## Command Line
Install the CLI:
```
go install go-micro.dev/v5/cmd/micro@latest
```
### Quick Start
```bash
micro new helloworld # Create a new service
cd helloworld
micro run # Run with API gateway
```
Then open http://localhost:8080 to see your service and call it from the browser.
### micro run
`micro run` starts your services with:
- **API Gateway** - HTTP to RPC proxy at `/api/{service}/{method}`
- **Web Dashboard** - Browse and call services at `/`
- **Health Checks** - Aggregated health at `/health`
- **Hot Reload** - Auto-rebuild on file changes
```bash
micro run # Gateway on :8080
micro run --address :3000 # Custom gateway port
micro run --no-gateway # Services only
micro run --env production # Use production environment
```
### Configuration
For multi-service projects, create a `micro.mu` file:
```
service users
path ./users
port 8081
service posts
path ./posts
port 8082
depends users
env development
DATABASE_URL sqlite://./dev.db
```
The gateway runs on :8080 by default, so services should use other ports.
### Deployment
Deploy to any Linux server with systemd:
```bash
# On your server (one-time setup)
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
# From your laptop
micro deploy user@your-server
```
The deploy command:
1. Builds binaries for Linux
2. Copies via SSH to the server
3. Sets up systemd services
4. Verifies services are healthy
Manage deployed services:
```bash
micro status --remote user@server # Check status
micro logs --remote user@server # View logs
micro logs myservice --remote user@server -f # Follow specific service
```
No Docker required. No Kubernetes. Just systemd.
See [docs/deployment.md](docs/deployment.md) for full deployment guide.
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
Docs: [`internal/website/docs`](internal/website/docs)
Package reference: https://pkg.go.dev/go-micro.dev/v5
Selected topics:
- Getting Started: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
- Plugins overview: [`internal/website/docs/plugins.md`](internal/website/docs/plugins.md)
- Learn by Example: [`internal/website/docs/examples/index.md`](internal/website/docs/examples/index.md)
## Adopters
- [Sourse](https://sourse.eu) - Work in the field of earth observation, including embedded Kubernetes running onboard aircraft, and weve built a mission management SaaS platform using Go Micro.
+36
View File
@@ -0,0 +1,36 @@
# Go Micro [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GoDoc](https://godoc.org/github.com/micro/go-micro?status.svg)](https://godoc.org/github.com/micro/go-micro) [![Travis CI](https://api.travis-ci.org/micro/go-micro.svg?branch=master)](https://travis-ci.org/micro/go-micro) [![Go Report Card](https://goreportcard.com/badge/micro/go-micro)](https://goreportcard.com/report/github.com/micro/go-micro)
Go Micro是基于Golang的微服务开发框架。
## 概览
Go Micro提供分布式系统开发的核心库,包含RPC与事件驱动的通信机制。
**micro**的设计哲学是可插拔的架构理念,她提供可快速构建系统的组件,并且可以根据自身的需求剥离默认实现并自行定制。
<img src="https://micro.mu/docs/images/go-micro.svg" />
所有插件可在仓库[github.com/micro/go-plugins](https://github.com/micro/go-plugins)中找到。
可以订阅我们的[Twitter](https://twitter.com/microhq)或者加入[Slack](http://slack.micro.mu/)论坛。
## 特性
Go Micro把分布式系统的各种细节抽象出来。下面是它的主要特性。
- **服务发现(Service Discovery** - 自动服务注册与名称解析。服务发现是微服务开发中的核心。当服务A要与服务B协作时,它得知道B在哪里。默认的服务发现系统是Consul,而multicast DNS (mdns,组播)机制作为本地解决方案,或者零依赖的P2P网络中的SWIM协议(gossip)。
- **负载均衡(Load Balancing)** - 在服务发现之上构建了负载均衡机制。当我们得到一个服务的任意多个的实例节点时,我们要一个机制去决定要路由到哪一个节点。我们使用随机处理过的哈希负载均衡机制来保证对服务请求颁发的均匀分布,并且在发生问题时进行重试。
- **消息编码(Message Encoding** - 支持基于内容类型(content-type)动态编码消息。客户端和服务端会一起使用content-type的格式来对Go进行无缝编/解码。各种各样的消息被编码会发送到不同的客户端,客户端服服务端默认会处理这些消息。content-type默认包含proto-rpc和json-rpc。
- **Request/Response** - RPC通信基于支持双向流的请求/响应方式,我们提供有抽象的同步通信机制。请求发送到服务时,会自动解析、负载均衡、拨号、转成字节流。默认的传输协议是http/1.1,而tls下使用http2协议。
- **异步消息(Async Messaging** - 发布订阅(PubSub)头等功能内置在异步通信与事件驱动架构中。事件通知在微服务开发中处于核心位置。默认的消息传送使用点到点http/1.1,激活tls时则使用http2。
- **可插拔接口(Pluggable Interfaces** - Go Micro为每个分布式系统抽象出接口。因此,Go Micro的接口都是可插拔的,允许其在运行时不可知的情况下仍可支持。所以只要实现接口,可以在内部使用任何的技术。更多插件请参考:[github.com/micro/go-plugins](https://github.com/micro/go-plugins)。
## 快速上手
更多关于架构、安装的资料可以查看[文档](https://micro.mu/docs/cn/)。
-163
View File
@@ -1,163 +0,0 @@
# Go Micro Roadmap
This roadmap outlines the planned features and improvements for Go Micro. Community feedback and contributions are welcome!
## Current Focus (Q1 2026)
### Documentation & Developer Experience
- [x] Modernize documentation structure
- [x] Add learn-by-example guides
- [x] Update issue templates
- [ ] Create video tutorials
- [ ] Interactive documentation site
- [ ] Plugin discovery dashboard
### Observability
- [ ] OpenTelemetry native support
- [ ] Auto-instrumentation for handlers
- [ ] Metrics export standardization
- [ ] Distributed tracing examples
- [ ] Integration with popular observability platforms
### Developer Tools
- [ ] `micro dev` with hot reload
- [ ] Service templates (`micro new --template`)
- [ ] Better error messages with suggestions
- [ ] Debug tooling improvements
- [ ] VS Code extension for Go Micro
## Q2 2026
### Production Readiness
- [ ] Health check standardization
- [ ] Graceful shutdown improvements
- [ ] Resource cleanup best practices
- [ ] Load testing framework integration
- [ ] Performance benchmarking suite
### Cloud Native
- [ ] Kubernetes operator
- [ ] Helm charts for common setups
- [ ] Service mesh integration guides (Istio, Linkerd)
- [ ] Cloud provider quickstarts (AWS, GCP, Azure)
- [ ] Multi-cluster patterns
### Security
- [ ] mTLS by default option
- [ ] Secret management integration (Vault, AWS Secrets Manager)
- [ ] RBAC improvements
- [ ] Security audit and hardening
- [ ] CVE scanning and response process
## Q3 2026
### Plugin Ecosystem
- [ ] Plugin marketplace/registry
- [ ] Plugin quality standards
- [ ] Community plugin contributions
- [ ] Plugin compatibility matrix
- [ ] Auto-discovery of available plugins
### Streaming & Async
- [ ] Improved streaming support
- [ ] Server-sent events (SSE) support
- [ ] WebSocket plugin
- [ ] Event sourcing patterns
- [ ] CQRS examples
### Testing
- [ ] Mock generation tooling
- [ ] Integration test helpers
- [ ] Contract testing support
- [ ] Chaos engineering examples
- [ ] E2E testing framework
## Q4 2026
### Performance
- [ ] Connection pooling optimizations
- [ ] Zero-allocation paths
- [ ] gRPC performance improvements
- [ ] Caching strategies guide
- [ ] Performance profiling tools
### Developer Productivity
- [ ] Code generation improvements
- [ ] Better IDE support
- [ ] Debugging tools
- [ ] Migration automation tools
- [ ] Upgrade helpers
### Community
- [ ] Regular blog posts and case studies
- [ ] Community spotlight program
- [ ] Contribution rewards
- [ ] Monthly community calls
- [ ] Conference presence
## Long-term Vision
### Core Framework
- Maintain backward compatibility (Go Micro v5+)
- Progressive disclosure of complexity
- Best-in-class developer experience
- Production-grade reliability
- Comprehensive plugin ecosystem
### Ecosystem Goals
- 100+ production deployments documented
- 50+ community plugins
- Active contributor community
- Regular releases (monthly patches, quarterly features)
- Comprehensive benchmarks vs alternatives
### Differentiation
- **Batteries included, fully swappable** - Start simple, scale complex
- **Zero-config local development** - No infrastructure required to start
- **Plugin ecosystem in-repo** - No version compatibility hell
- **Progressive complexity** - Learn as you grow
- **Cloud-native first** - Built for Kubernetes and containers
## Contributing
We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
### High Priority Areas
1. Documentation improvements
2. Real-world examples
3. Plugin development
4. Performance optimizations
5. Testing infrastructure
### How to Contribute
- Pick an item from the roadmap
- Open an issue to discuss approach
- Submit a PR with implementation
- Help review others' contributions
## Feedback
Have suggestions for the roadmap?
- Open a [feature request](.github/ISSUE_TEMPLATE/feature_request.md)
- Start a discussion in GitHub Discussions
- Comment on existing roadmap issues
## Version Compatibility
We follow semantic versioning:
- Major versions (v5 → v6): Breaking changes
- Minor versions (v5.3 → v5.4): New features, backward compatible
- Patch versions (v5.3.0 → v5.3.1): Bug fixes, no API changes
## Support Timeline
- v5: Active development (current)
- v4: Security fixes only (until v6 release)
- v3: End of life
---
Last updated: November 2025
This roadmap is subject to change based on community needs and priorities. Star the repo to stay updated! ⭐
+2
View File
@@ -0,0 +1,2 @@
// Package agent provides an interface for building robots
package agent
+54
View File
@@ -0,0 +1,54 @@
// Package command is an interface for defining bot commands
package command
var (
// Commmands keyed by golang/regexp patterns
// regexp.Match(key, input) is used to match
Commands = map[string]Command{}
)
// Command is the interface for specific named
// commands executed via plugins or the bot.
type Command interface {
// Executes the command with args passed in
Exec(args ...string) ([]byte, error)
// Usage of the command
Usage() string
// Description of the command
Description() string
// Name of the command
String() string
}
type cmd struct {
name string
usage string
description string
exec func(args ...string) ([]byte, error)
}
func (c *cmd) Description() string {
return c.description
}
func (c *cmd) Exec(args ...string) ([]byte, error) {
return c.exec(args...)
}
func (c *cmd) Usage() string {
return c.usage
}
func (c *cmd) String() string {
return c.name
}
// NewCommand helps quickly create a new command
func NewCommand(name, usage, description string, exec func(args ...string) ([]byte, error)) Command {
return &cmd{
name: name,
usage: usage,
description: description,
exec: exec,
}
}
+65
View File
@@ -0,0 +1,65 @@
package command
import (
"testing"
)
func TestCommand(t *testing.T) {
c := &cmd{
name: "test",
usage: "test usage",
description: "test description",
exec: func(args ...string) ([]byte, error) {
return []byte("test"), nil
},
}
if c.String() != c.name {
t.Fatalf("expected name %s got %s", c.name, c.String())
}
if c.Usage() != c.usage {
t.Fatalf("expected usage %s got %s", c.usage, c.Usage())
}
if c.Description() != c.description {
t.Fatalf("expected description %s got %s", c.description, c.Description())
}
if r, err := c.Exec(); err != nil {
t.Fatal(err)
} else if string(r) != "test" {
t.Fatalf("expected exec result test got %s", string(r))
}
}
func TestNewCommand(t *testing.T) {
c := &cmd{
name: "test",
usage: "test usage",
description: "test description",
exec: func(args ...string) ([]byte, error) {
return []byte("test"), nil
},
}
nc := NewCommand(c.name, c.usage, c.description, c.exec)
if nc.String() != c.name {
t.Fatalf("expected name %s got %s", c.name, nc.String())
}
if nc.Usage() != c.usage {
t.Fatalf("expected usage %s got %s", c.usage, nc.Usage())
}
if nc.Description() != c.description {
t.Fatalf("expected description %s got %s", c.description, nc.Description())
}
if r, err := nc.Exec(); err != nil {
t.Fatal(err)
} else if string(r) != "test" {
t.Fatalf("expected exec result test got %s", string(r))
}
}
+22
View File
@@ -0,0 +1,22 @@
# Discord input for micro-bot
[Discord](https://discordapp.com) support for micro bot based on [discordgo](github.com/bwmarrin/discordgo).
This was originally written by Aleksandr Tihomirov (@zet4) and can be found at https://github.com/zet4/micro-misc/.
## Options
### discord_token
You have to supply an application token via `--discord_token`.
Head over to Discord's [developer introduction](https://discordapp.com/developers/docs/intro)
to learn how to create applications and how the API works.
### discord_prefix
Set a command prefix with `--discord_prefix`. The default prefix is `Micro `.
You can mention the bot or use the prefix to run a command.
### discord_whitelist
Pass a list of comma-separated user IDs with `--discord_whitelist`. Only allow
these users to use the bot.
+94
View File
@@ -0,0 +1,94 @@
package discord
import (
"errors"
"strings"
"sync"
"github.com/bwmarrin/discordgo"
"github.com/micro/go-micro/agent/input"
"github.com/micro/go-micro/util/log"
)
type discordConn struct {
master *discordInput
exit chan struct{}
recv chan *discordgo.Message
sync.Mutex
}
func newConn(master *discordInput) *discordConn {
conn := &discordConn{
master: master,
exit: make(chan struct{}),
recv: make(chan *discordgo.Message),
}
conn.master.session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
if m.Author.ID == master.botID {
return
}
whitelisted := false
for _, ID := range conn.master.whitelist {
if m.Author.ID == ID {
whitelisted = true
break
}
}
if len(master.whitelist) > 0 && !whitelisted {
return
}
var valid bool
m.Message.Content, valid = conn.master.prefixfn(m.Message.Content)
if !valid {
return
}
conn.recv <- m.Message
})
return conn
}
func (dc *discordConn) Recv(event *input.Event) error {
for {
select {
case <-dc.exit:
return errors.New("connection closed")
case msg := <-dc.recv:
event.From = msg.ChannelID + ":" + msg.Author.ID
event.To = dc.master.botID
event.Type = input.TextEvent
event.Data = []byte(msg.Content)
return nil
}
}
}
func (dc *discordConn) Send(e *input.Event) error {
fields := strings.Split(e.To, ":")
_, err := dc.master.session.ChannelMessageSend(fields[0], string(e.Data))
if err != nil {
log.Log("[bot][loop][send]", err)
}
return nil
}
func (dc *discordConn) Close() error {
if err := dc.master.session.Close(); err != nil {
return err
}
select {
case <-dc.exit:
return nil
default:
close(dc.exit)
}
return nil
}
+153
View File
@@ -0,0 +1,153 @@
package discord
import (
"fmt"
"sync"
"errors"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/micro/cli"
"github.com/micro/go-micro/agent/input"
)
func init() {
input.Inputs["discord"] = newInput()
}
func newInput() *discordInput {
return &discordInput{}
}
type discordInput struct {
token string
whitelist []string
prefix string
prefixfn func(string) (string, bool)
botID string
session *discordgo.Session
sync.Mutex
running bool
exit chan struct{}
}
func (d *discordInput) Flags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "discord_token",
EnvVar: "MICRO_DISCORD_TOKEN",
Usage: "Discord token (prefix with Bot if it's for bot account)",
},
cli.StringFlag{
Name: "discord_whitelist",
EnvVar: "MICRO_DISCORD_WHITELIST",
Usage: "Discord Whitelist (seperated by ,)",
},
cli.StringFlag{
Name: "discord_prefix",
Usage: "Discord Prefix",
EnvVar: "MICRO_DISCORD_PREFIX",
Value: "Micro ",
},
}
}
func (d *discordInput) Init(ctx *cli.Context) error {
token := ctx.String("discord_token")
whitelist := ctx.String("discord_whitelist")
prefix := ctx.String("discord_prefix")
if len(token) == 0 {
return errors.New("require token")
}
d.token = token
d.prefix = prefix
if len(whitelist) > 0 {
d.whitelist = strings.Split(whitelist, ",")
}
return nil
}
func (d *discordInput) Start() error {
if len(d.token) == 0 {
return errors.New("missing discord configuration")
}
d.Lock()
defer d.Unlock()
if d.running {
return nil
}
var err error
d.session, err = discordgo.New(d.token)
if err != nil {
return err
}
u, err := d.session.User("@me")
if err != nil {
return err
}
d.botID = u.ID
d.prefixfn = CheckPrefixFactory(fmt.Sprintf("<@%s> ", d.botID), fmt.Sprintf("<@!%s> ", d.botID), d.prefix)
d.exit = make(chan struct{})
d.running = true
return nil
}
func (d *discordInput) Stream() (input.Conn, error) {
d.Lock()
defer d.Unlock()
if !d.running {
return nil, errors.New("not running")
}
//Fire-n-forget close just in case...
d.session.Close()
conn := newConn(d)
if err := d.session.Open(); err != nil {
return nil, err
}
return conn, nil
}
func (d *discordInput) Stop() error {
d.Lock()
defer d.Unlock()
if !d.running {
return nil
}
close(d.exit)
d.running = false
return nil
}
func (d *discordInput) String() string {
return "discord"
}
// CheckPrefixFactory Creates a prefix checking function and stuff.
func CheckPrefixFactory(prefixes ...string) func(string) (string, bool) {
return func(content string) (string, bool) {
for _, prefix := range prefixes {
if strings.HasPrefix(content, prefix) {
return strings.TrimPrefix(content, prefix), true
}
}
return "", false
}
}
+55
View File
@@ -0,0 +1,55 @@
// Package input is an interface for bot inputs
package input
import (
"github.com/micro/cli"
)
type EventType string
const (
TextEvent EventType = "text"
)
var (
// Inputs keyed by name
// Example slack or hipchat
Inputs = map[string]Input{}
)
// Event is the unit sent and received
type Event struct {
Type EventType
From string
To string
Data []byte
Meta map[string]interface{}
}
// Input is an interface for sources which
// provide a way to communicate with the bot.
// Slack, HipChat, XMPP, etc.
type Input interface {
// Provide cli flags
Flags() []cli.Flag
// Initialise input using cli context
Init(*cli.Context) error
// Stream events from the input
Stream() (Conn, error)
// Start the input
Start() error
// Stop the input
Stop() error
// name of the input
String() string
}
// Conn interface provides a way to
// send and receive events. Send and
// Recv both block until succeeding
// or failing.
type Conn interface {
Close() error
Recv(*Event) error
Send(*Event) error
}
+160
View File
@@ -0,0 +1,160 @@
package slack
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/micro/go-micro/agent/input"
"github.com/nlopes/slack"
)
// Satisfies the input.Conn interface
type slackConn struct {
auth *slack.AuthTestResponse
rtm *slack.RTM
exit chan bool
sync.Mutex
names map[string]string
}
func (s *slackConn) run() {
// func retrieves user names and maps to IDs
setNames := func() {
names := make(map[string]string)
users, err := s.rtm.Client.GetUsers()
if err != nil {
return
}
for _, user := range users {
names[user.ID] = user.Name
}
s.Lock()
s.names = names
s.Unlock()
}
setNames()
t := time.NewTicker(time.Minute)
defer t.Stop()
for {
select {
case <-s.exit:
return
case <-t.C:
setNames()
}
}
}
func (s *slackConn) getName(id string) string {
s.Lock()
name := s.names[id]
s.Unlock()
return name
}
func (s *slackConn) Close() error {
select {
case <-s.exit:
return nil
default:
close(s.exit)
}
return nil
}
func (s *slackConn) Recv(event *input.Event) error {
if event == nil {
return errors.New("event cannot be nil")
}
for {
select {
case <-s.exit:
return errors.New("connection closed")
case e := <-s.rtm.IncomingEvents:
switch ev := e.Data.(type) {
case *slack.MessageEvent:
// only accept type message
if ev.Type != "message" {
continue
}
// only accept DMs or messages to me
switch {
case strings.HasPrefix(ev.Channel, "D"):
case strings.HasPrefix(ev.Text, s.auth.User):
case strings.HasPrefix(ev.Text, fmt.Sprintf("<@%s>", s.auth.UserID)):
default:
continue
}
// Strip username from text
switch {
case strings.HasPrefix(ev.Text, s.auth.User):
args := strings.Split(ev.Text, " ")[1:]
ev.Text = strings.Join(args, " ")
event.To = s.auth.User
case strings.HasPrefix(ev.Text, fmt.Sprintf("<@%s>", s.auth.UserID)):
args := strings.Split(ev.Text, " ")[1:]
ev.Text = strings.Join(args, " ")
event.To = s.auth.UserID
}
if event.Meta == nil {
event.Meta = make(map[string]interface{})
}
// fill in the blanks
event.From = ev.Channel + ":" + ev.User
event.Type = input.TextEvent
event.Data = []byte(ev.Text)
event.Meta["reply"] = ev
return nil
case *slack.InvalidAuthEvent:
return errors.New("invalid credentials")
}
}
}
}
func (s *slackConn) Send(event *input.Event) error {
var channel, message, name string
if len(event.To) == 0 {
return errors.New("require Event.To")
}
parts := strings.Split(event.To, ":")
if len(parts) == 2 {
channel = parts[0]
name = s.getName(parts[1])
// try using reply meta
} else if ev, ok := event.Meta["reply"]; ok {
channel = ev.(*slack.MessageEvent).Channel
name = s.getName(ev.(*slack.MessageEvent).User)
}
// don't know where to send the message
if len(channel) == 0 {
return errors.New("could not determine who message is to")
}
if len(name) == 0 || strings.HasPrefix(channel, "D") {
message = string(event.Data)
} else {
message = fmt.Sprintf("@%s: %s", name, string(event.Data))
}
s.rtm.SendMessage(s.rtm.NewOutgoingMessage(message, channel))
return nil
}
+147
View File
@@ -0,0 +1,147 @@
package slack
import (
"errors"
"sync"
"github.com/micro/cli"
"github.com/micro/go-micro/agent/input"
"github.com/nlopes/slack"
)
type slackInput struct {
debug bool
token string
sync.Mutex
running bool
exit chan bool
api *slack.Client
}
func init() {
input.Inputs["slack"] = NewInput()
}
func (p *slackInput) Flags() []cli.Flag {
return []cli.Flag{
cli.BoolFlag{
Name: "slack_debug",
Usage: "Slack debug output",
EnvVar: "MICRO_SLACK_DEBUG",
},
cli.StringFlag{
Name: "slack_token",
Usage: "Slack token",
EnvVar: "MICRO_SLACK_TOKEN",
},
}
}
func (p *slackInput) Init(ctx *cli.Context) error {
debug := ctx.Bool("slack_debug")
token := ctx.String("slack_token")
if len(token) == 0 {
return errors.New("missing slack token")
}
p.debug = debug
p.token = token
return nil
}
func (p *slackInput) Stream() (input.Conn, error) {
p.Lock()
defer p.Unlock()
if !p.running {
return nil, errors.New("not running")
}
// test auth
auth, err := p.api.AuthTest()
if err != nil {
return nil, err
}
rtm := p.api.NewRTM()
exit := make(chan bool)
go rtm.ManageConnection()
go func() {
select {
case <-p.exit:
select {
case <-exit:
return
default:
close(exit)
}
case <-exit:
}
rtm.Disconnect()
}()
conn := &slackConn{
auth: auth,
rtm: rtm,
exit: exit,
names: make(map[string]string),
}
go conn.run()
return conn, nil
}
func (p *slackInput) Start() error {
if len(p.token) == 0 {
return errors.New("missing slack token")
}
p.Lock()
defer p.Unlock()
if p.running {
return nil
}
api := slack.New(p.token, slack.OptionDebug(p.debug))
// test auth
_, err := api.AuthTest()
if err != nil {
return err
}
p.api = api
p.exit = make(chan bool)
p.running = true
return nil
}
func (p *slackInput) Stop() error {
p.Lock()
defer p.Unlock()
if !p.running {
return nil
}
close(p.exit)
p.running = false
return nil
}
func (p *slackInput) String() string {
return "slack"
}
func NewInput() input.Input {
return &slackInput{}
}
+18
View File
@@ -0,0 +1,18 @@
# Telegram Messenger input for micro bot
[Telegram](https://telegram.org) support for micro bot based on [telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api).
## Options
### --telegram_token (required)
Sets bot's token for interacting with API.
Head over to Telegram's [API documentation](https://core.telegram.org/bots/api)
to learn how to create bots and how the API works.
### --telegram_debug
Sets the debug flag to make the bot's output verbose.
### --telegram_whitelist
Sets a list of comma-separated nicknames (without @ symbol in the beginning) for interacting with bot. Only these users can use the bot.
+115
View File
@@ -0,0 +1,115 @@
package telegram
import (
"errors"
"strings"
"sync"
"github.com/forestgiant/sliceutil"
"github.com/micro/go-micro/agent/input"
"github.com/micro/go-micro/util/log"
"gopkg.in/telegram-bot-api.v4"
)
type telegramConn struct {
input *telegramInput
recv <-chan tgbotapi.Update
exit chan bool
syncCond *sync.Cond
mutex sync.Mutex
}
func newConn(input *telegramInput) (*telegramConn, error) {
conn := &telegramConn{
input: input,
}
conn.syncCond = sync.NewCond(&conn.mutex)
go conn.run()
return conn, nil
}
func (tc *telegramConn) run() {
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := tc.input.api.GetUpdatesChan(u)
if err != nil {
return
}
tc.recv = updates
tc.syncCond.Signal()
for {
select {
case <-tc.exit:
return
}
}
}
func (tc *telegramConn) Close() error {
return nil
}
func (tc *telegramConn) Recv(event *input.Event) error {
if event == nil {
return errors.New("event cannot be nil")
}
for {
if tc.recv == nil {
tc.mutex.Lock()
tc.syncCond.Wait()
}
update := <-tc.recv
if update.Message == nil || (len(tc.input.whitelist) > 0 && !sliceutil.Contains(tc.input.whitelist, update.Message.From.UserName)) {
continue
}
if event.Meta == nil {
event.Meta = make(map[string]interface{})
}
event.Type = input.TextEvent
event.From = update.Message.From.UserName
event.To = tc.input.api.Self.UserName
event.Data = []byte(update.Message.Text)
event.Meta["chatId"] = update.Message.Chat.ID
event.Meta["chatType"] = update.Message.Chat.Type
event.Meta["messageId"] = update.Message.MessageID
return nil
}
}
func (tc *telegramConn) Send(event *input.Event) error {
messageText := strings.TrimSpace(string(event.Data))
chatId := event.Meta["chatId"].(int64)
chatType := ChatType(event.Meta["chatType"].(string))
msgConfig := tgbotapi.NewMessage(chatId, messageText)
msgConfig.ParseMode = tgbotapi.ModeHTML
if sliceutil.Contains([]ChatType{Group, Supergroup}, chatType) {
msgConfig.ReplyToMessageID = event.Meta["messageId"].(int)
}
_, err := tc.input.api.Send(msgConfig)
if err != nil {
// probably it could be because of nested HTML tags -- telegram doesn't allow nested tags
log.Log("[telegram][Send] error:", err)
msgConfig.Text = "This bot couldn't send the response (Internal error)"
tc.input.api.Send(msgConfig)
}
return nil
}
+101
View File
@@ -0,0 +1,101 @@
package telegram
import (
"errors"
"strings"
"sync"
"github.com/micro/cli"
"github.com/micro/go-micro/agent/input"
"gopkg.in/telegram-bot-api.v4"
)
type telegramInput struct {
sync.Mutex
debug bool
token string
whitelist []string
api *tgbotapi.BotAPI
}
type ChatType string
const (
Private ChatType = "private"
Group ChatType = "group"
Supergroup ChatType = "supergroup"
)
func init() {
input.Inputs["telegram"] = &telegramInput{}
}
func (ti *telegramInput) Flags() []cli.Flag {
return []cli.Flag{
cli.BoolFlag{
Name: "telegram_debug",
EnvVar: "MICRO_TELEGRAM_DEBUG",
Usage: "Telegram debug output",
},
cli.StringFlag{
Name: "telegram_token",
EnvVar: "MICRO_TELEGRAM_TOKEN",
Usage: "Telegram token",
},
cli.StringFlag{
Name: "telegram_whitelist",
EnvVar: "MICRO_TELEGRAM_WHITELIST",
Usage: "Telegram bot's users (comma-separated values)",
},
}
}
func (ti *telegramInput) Init(ctx *cli.Context) error {
ti.debug = ctx.Bool("telegram_debug")
ti.token = ctx.String("telegram_token")
whitelist := ctx.String("telegram_whitelist")
if whitelist != "" {
ti.whitelist = strings.Split(whitelist, ",")
}
if len(ti.token) == 0 {
return errors.New("missing telegram token")
}
return nil
}
func (ti *telegramInput) Stream() (input.Conn, error) {
ti.Lock()
defer ti.Unlock()
return newConn(ti)
}
func (ti *telegramInput) Start() error {
ti.Lock()
defer ti.Unlock()
api, err := tgbotapi.NewBotAPI(ti.token)
if err != nil {
return err
}
ti.api = api
api.Debug = ti.debug
return nil
}
func (ti *telegramInput) Stop() error {
return nil
}
func (p *telegramInput) String() string {
return "telegram"
}
+118
View File
@@ -0,0 +1,118 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: github.com/micro/go-bot/proto/bot.proto
/*
Package go_micro_bot is a generated protocol buffer package.
It is generated from these files:
github.com/micro/go-bot/proto/bot.proto
It has these top-level messages:
HelpRequest
HelpResponse
ExecRequest
ExecResponse
*/
package go_micro_bot
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "context"
client "github.com/micro/go-micro/client"
server "github.com/micro/go-micro/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ client.Option
var _ server.Option
// Client API for Command service
type CommandService interface {
Help(ctx context.Context, in *HelpRequest, opts ...client.CallOption) (*HelpResponse, error)
Exec(ctx context.Context, in *ExecRequest, opts ...client.CallOption) (*ExecResponse, error)
}
type commandService struct {
c client.Client
name string
}
func NewCommandService(name string, c client.Client) CommandService {
if c == nil {
c = client.NewClient()
}
if len(name) == 0 {
name = "go.micro.bot"
}
return &commandService{
c: c,
name: name,
}
}
func (c *commandService) Help(ctx context.Context, in *HelpRequest, opts ...client.CallOption) (*HelpResponse, error) {
req := c.c.NewRequest(c.name, "Command.Help", in)
out := new(HelpResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *commandService) Exec(ctx context.Context, in *ExecRequest, opts ...client.CallOption) (*ExecResponse, error) {
req := c.c.NewRequest(c.name, "Command.Exec", in)
out := new(ExecResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Command service
type CommandHandler interface {
Help(context.Context, *HelpRequest, *HelpResponse) error
Exec(context.Context, *ExecRequest, *ExecResponse) error
}
func RegisterCommandHandler(s server.Server, hdlr CommandHandler, opts ...server.HandlerOption) error {
type _command interface {
Help(ctx context.Context, in *HelpRequest, out *HelpResponse) error
Exec(ctx context.Context, in *ExecRequest, out *ExecResponse) error
}
type Command struct {
_command
}
h := &commandHandler{hdlr}
return s.Handle(s.NewHandler(&Command{h}, opts...))
}
type commandHandler struct {
CommandHandler
}
func (h *commandHandler) Help(ctx context.Context, in *HelpRequest, out *HelpResponse) error {
return h.CommandHandler.Help(ctx, in, out)
}
func (h *commandHandler) Exec(ctx context.Context, in *ExecRequest, out *ExecResponse) error {
return h.CommandHandler.Exec(ctx, in, out)
}
+210
View File
@@ -0,0 +1,210 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/micro/go-bot/proto/bot.proto
package go_micro_bot
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type HelpRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelpRequest) Reset() { *m = HelpRequest{} }
func (m *HelpRequest) String() string { return proto.CompactTextString(m) }
func (*HelpRequest) ProtoMessage() {}
func (*HelpRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_bot_654832eab83ed4b5, []int{0}
}
func (m *HelpRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelpRequest.Unmarshal(m, b)
}
func (m *HelpRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelpRequest.Marshal(b, m, deterministic)
}
func (dst *HelpRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelpRequest.Merge(dst, src)
}
func (m *HelpRequest) XXX_Size() int {
return xxx_messageInfo_HelpRequest.Size(m)
}
func (m *HelpRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HelpRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HelpRequest proto.InternalMessageInfo
type HelpResponse struct {
Usage string `protobuf:"bytes,1,opt,name=usage,proto3" json:"usage,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelpResponse) Reset() { *m = HelpResponse{} }
func (m *HelpResponse) String() string { return proto.CompactTextString(m) }
func (*HelpResponse) ProtoMessage() {}
func (*HelpResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_bot_654832eab83ed4b5, []int{1}
}
func (m *HelpResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelpResponse.Unmarshal(m, b)
}
func (m *HelpResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelpResponse.Marshal(b, m, deterministic)
}
func (dst *HelpResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelpResponse.Merge(dst, src)
}
func (m *HelpResponse) XXX_Size() int {
return xxx_messageInfo_HelpResponse.Size(m)
}
func (m *HelpResponse) XXX_DiscardUnknown() {
xxx_messageInfo_HelpResponse.DiscardUnknown(m)
}
var xxx_messageInfo_HelpResponse proto.InternalMessageInfo
func (m *HelpResponse) GetUsage() string {
if m != nil {
return m.Usage
}
return ""
}
func (m *HelpResponse) GetDescription() string {
if m != nil {
return m.Description
}
return ""
}
type ExecRequest struct {
Args []string `protobuf:"bytes,1,rep,name=args,proto3" json:"args,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ExecRequest) Reset() { *m = ExecRequest{} }
func (m *ExecRequest) String() string { return proto.CompactTextString(m) }
func (*ExecRequest) ProtoMessage() {}
func (*ExecRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_bot_654832eab83ed4b5, []int{2}
}
func (m *ExecRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ExecRequest.Unmarshal(m, b)
}
func (m *ExecRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ExecRequest.Marshal(b, m, deterministic)
}
func (dst *ExecRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ExecRequest.Merge(dst, src)
}
func (m *ExecRequest) XXX_Size() int {
return xxx_messageInfo_ExecRequest.Size(m)
}
func (m *ExecRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ExecRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ExecRequest proto.InternalMessageInfo
func (m *ExecRequest) GetArgs() []string {
if m != nil {
return m.Args
}
return nil
}
type ExecResponse struct {
Result []byte `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ExecResponse) Reset() { *m = ExecResponse{} }
func (m *ExecResponse) String() string { return proto.CompactTextString(m) }
func (*ExecResponse) ProtoMessage() {}
func (*ExecResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_bot_654832eab83ed4b5, []int{3}
}
func (m *ExecResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ExecResponse.Unmarshal(m, b)
}
func (m *ExecResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ExecResponse.Marshal(b, m, deterministic)
}
func (dst *ExecResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ExecResponse.Merge(dst, src)
}
func (m *ExecResponse) XXX_Size() int {
return xxx_messageInfo_ExecResponse.Size(m)
}
func (m *ExecResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ExecResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ExecResponse proto.InternalMessageInfo
func (m *ExecResponse) GetResult() []byte {
if m != nil {
return m.Result
}
return nil
}
func (m *ExecResponse) GetError() string {
if m != nil {
return m.Error
}
return ""
}
func init() {
proto.RegisterType((*HelpRequest)(nil), "go.micro.bot.HelpRequest")
proto.RegisterType((*HelpResponse)(nil), "go.micro.bot.HelpResponse")
proto.RegisterType((*ExecRequest)(nil), "go.micro.bot.ExecRequest")
proto.RegisterType((*ExecResponse)(nil), "go.micro.bot.ExecResponse")
}
func init() {
proto.RegisterFile("github.com/micro/go-bot/proto/bot.proto", fileDescriptor_bot_654832eab83ed4b5)
}
var fileDescriptor_bot_654832eab83ed4b5 = []byte{
// 246 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x41, 0x4b, 0xc4, 0x30,
0x10, 0x85, 0xb7, 0xba, 0xae, 0xec, 0xb4, 0x5e, 0x82, 0x48, 0xdd, 0x53, 0xcd, 0xc5, 0xbd, 0x98,
0x82, 0x5e, 0x05, 0x0f, 0xa2, 0x78, 0xee, 0x3f, 0x68, 0xba, 0x43, 0x2c, 0x6c, 0x3b, 0x35, 0x99,
0x82, 0xff, 0xc1, 0x3f, 0x2d, 0x4d, 0x72, 0x08, 0xcb, 0xde, 0xe6, 0x65, 0x86, 0xf7, 0xbe, 0x17,
0x78, 0x34, 0x3d, 0x7f, 0xcf, 0x5a, 0x75, 0x34, 0xd4, 0x43, 0xdf, 0x59, 0xaa, 0x0d, 0x3d, 0x69,
0xe2, 0x7a, 0xb2, 0xc4, 0x54, 0x6b, 0x62, 0xe5, 0x27, 0x51, 0x18, 0x52, 0xfe, 0x40, 0x69, 0x62,
0x79, 0x03, 0xf9, 0x17, 0x1e, 0xa7, 0x06, 0x7f, 0x66, 0x74, 0x2c, 0x3f, 0xa1, 0x08, 0xd2, 0x4d,
0x34, 0x3a, 0x14, 0xb7, 0x70, 0x35, 0xbb, 0xd6, 0x60, 0x99, 0x55, 0xd9, 0x7e, 0xdb, 0x04, 0x21,
0x2a, 0xc8, 0x0f, 0xe8, 0x3a, 0xdb, 0x4f, 0xdc, 0xd3, 0x58, 0x5e, 0xf8, 0x5d, 0xfa, 0x24, 0x1f,
0x20, 0xff, 0xf8, 0xc5, 0x2e, 0xda, 0x0a, 0x01, 0xeb, 0xd6, 0x1a, 0x57, 0x66, 0xd5, 0xe5, 0x7e,
0xdb, 0xf8, 0x59, 0xbe, 0x42, 0x11, 0x4e, 0x62, 0xd4, 0x1d, 0x6c, 0x2c, 0xba, 0xf9, 0xc8, 0x3e,
0xab, 0x68, 0xa2, 0x5a, 0x10, 0xd0, 0x5a, 0xb2, 0x31, 0x26, 0x88, 0xe7, 0xbf, 0x0c, 0xae, 0xdf,
0x69, 0x18, 0xda, 0xf1, 0x20, 0xde, 0x60, 0xbd, 0x40, 0x8b, 0x7b, 0x95, 0x56, 0x53, 0x49, 0xaf,
0xdd, 0xee, 0xdc, 0x2a, 0x04, 0xcb, 0xd5, 0x62, 0xb0, 0xa0, 0x9c, 0x1a, 0x24, 0x0d, 0x4e, 0x0d,
0x52, 0x72, 0xb9, 0xd2, 0x1b, 0xff, 0xb5, 0x2f, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0xcb, 0x77,
0xdf, 0x28, 0x85, 0x01, 0x00, 0x00,
}
+25
View File
@@ -0,0 +1,25 @@
syntax = "proto3";
package go.micro.bot;
service Command {
rpc Help(HelpRequest) returns (HelpResponse) {};
rpc Exec(ExecRequest) returns (ExecResponse) {};
}
message HelpRequest {
}
message HelpResponse {
string usage = 1;
string description = 2;
}
message ExecRequest {
repeated string args = 1;
}
message ExecResponse {
bytes result = 1;
string error = 2;
}
+144
View File
@@ -0,0 +1,144 @@
package api
import (
"errors"
"regexp"
"strings"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/server"
)
// Endpoint is a mapping between an RPC method and HTTP endpoint
type Endpoint struct {
// RPC Method e.g. Greeter.Hello
Name string
// Description e.g what's this endpoint for
Description string
// API Handler e.g rpc, proxy
Handler string
// HTTP Host e.g example.com
Host []string
// HTTP Methods e.g GET, POST
Method []string
// HTTP Path e.g /greeter. Expect POSIX regex
Path []string
}
// Service represents an API service
type Service struct {
// Name of service
Name string
// The endpoint for this service
Endpoint *Endpoint
// Versions of this service
Services []*registry.Service
}
func strip(s string) string {
return strings.TrimSpace(s)
}
func slice(s string) []string {
var sl []string
for _, p := range strings.Split(s, ",") {
if str := strip(p); len(str) > 0 {
sl = append(sl, strip(p))
}
}
return sl
}
// Encode encodes an endpoint to endpoint metadata
func Encode(e *Endpoint) map[string]string {
if e == nil {
return nil
}
return map[string]string{
"endpoint": e.Name,
"description": e.Description,
"method": strings.Join(e.Method, ","),
"path": strings.Join(e.Path, ","),
"host": strings.Join(e.Host, ","),
"handler": e.Handler,
}
}
// Decode decodes endpoint metadata into an endpoint
func Decode(e map[string]string) *Endpoint {
if e == nil {
return nil
}
return &Endpoint{
Name: e["endpoint"],
Description: e["description"],
Method: slice(e["method"]),
Path: slice(e["path"]),
Host: slice(e["host"]),
Handler: e["handler"],
}
}
// Validate validates an endpoint to guarantee it won't blow up when being served
func Validate(e *Endpoint) error {
if e == nil {
return errors.New("endpoint is nil")
}
if len(e.Name) == 0 {
return errors.New("name required")
}
for _, p := range e.Path {
_, err := regexp.CompilePOSIX(p)
if err != nil {
return err
}
}
if len(e.Handler) == 0 {
return errors.New("invalid handler")
}
return nil
}
/*
Design ideas
// Gateway is an api gateway interface
type Gateway interface {
// Register a http handler
Handle(pattern string, http.Handler)
// Register a route
RegisterRoute(r Route)
// Init initialises the command line.
// It also parses further options.
Init(...Option) error
// Run the gateway
Run() error
}
// NewGateway returns a new api gateway
func NewGateway() Gateway {
return newGateway()
}
*/
// WithEndpoint returns a server.HandlerOption with endpoint metadata set
//
// Usage:
//
// proto.RegisterHandler(service.Server(), new(Handler), api.WithEndpoint(
// &api.Endpoint{
// Name: "Greeter.Hello",
// Path: []string{"/greeter"},
// },
// ))
func WithEndpoint(e *Endpoint) server.HandlerOption {
return server.EndpointMetadata(e.Name, Encode(e))
}
+113
View File
@@ -0,0 +1,113 @@
package api
import (
"strings"
"testing"
)
func TestEncoding(t *testing.T) {
testData := []*Endpoint{
nil,
{
Name: "Foo.Bar",
Description: "A test endpoint",
Handler: "meta",
Host: []string{"foo.com"},
Method: []string{"GET"},
Path: []string{"/test"},
},
}
compare := func(expect, got []string) bool {
// no data to compare, return true
if len(expect) == 0 && len(got) == 0 {
return true
}
// no data expected but got some return false
if len(expect) == 0 && len(got) > 0 {
return false
}
// compare expected with what we got
for _, e := range expect {
var seen bool
for _, g := range got {
if e == g {
seen = true
break
}
}
if !seen {
return false
}
}
// we're done, return true
return true
}
for _, d := range testData {
// encode
e := Encode(d)
// decode
de := Decode(e)
// nil endpoint returns nil
if d == nil {
if e != nil {
t.Fatalf("expected nil got %v", e)
}
if de != nil {
t.Fatalf("expected nil got %v", de)
}
continue
}
// check encoded map
name := e["endpoint"]
desc := e["description"]
method := strings.Split(e["method"], ",")
path := strings.Split(e["path"], ",")
host := strings.Split(e["host"], ",")
handler := e["handler"]
if name != d.Name {
t.Fatalf("expected %v got %v", d.Name, name)
}
if desc != d.Description {
t.Fatalf("expected %v got %v", d.Description, desc)
}
if handler != d.Handler {
t.Fatalf("expected %v got %v", d.Handler, handler)
}
if ok := compare(d.Method, method); !ok {
t.Fatalf("expected %v got %v", d.Method, method)
}
if ok := compare(d.Path, path); !ok {
t.Fatalf("expected %v got %v", d.Path, path)
}
if ok := compare(d.Host, host); !ok {
t.Fatalf("expected %v got %v", d.Host, host)
}
if de.Name != d.Name {
t.Fatalf("expected %v got %v", d.Name, de.Name)
}
if de.Description != d.Description {
t.Fatalf("expected %v got %v", d.Description, de.Description)
}
if de.Handler != d.Handler {
t.Fatalf("expected %v got %v", d.Handler, de.Handler)
}
if ok := compare(d.Method, de.Method); !ok {
t.Fatalf("expected %v got %v", d.Method, de.Method)
}
if ok := compare(d.Path, de.Path); !ok {
t.Fatalf("expected %v got %v", d.Path, de.Path)
}
if ok := compare(d.Host, de.Host); !ok {
t.Fatalf("expected %v got %v", d.Host, de.Host)
}
}
}
+117
View File
@@ -0,0 +1,117 @@
// Package api provides an http-rpc handler which provides the entire http request over rpc
package api
import (
"net/http"
goapi "github.com/micro/go-micro/api"
"github.com/micro/go-micro/api/handler"
api "github.com/micro/go-micro/api/proto"
"github.com/micro/go-micro/client"
"github.com/micro/go-micro/client/selector"
"github.com/micro/go-micro/errors"
"github.com/micro/go-micro/util/ctx"
)
type apiHandler struct {
opts handler.Options
s *goapi.Service
}
const (
Handler = "api"
)
// API handler is the default handler which takes api.Request and returns api.Response
func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
request, err := requestToProto(r)
if err != nil {
er := errors.InternalServerError("go.micro.api", err.Error())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.Write([]byte(er.Error()))
return
}
var service *goapi.Service
if a.s != nil {
// we were given the service
service = a.s
} else if a.opts.Router != nil {
// try get service from router
s, err := a.opts.Router.Route(r)
if err != nil {
er := errors.InternalServerError("go.micro.api", err.Error())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.Write([]byte(er.Error()))
return
}
service = s
} else {
// we have no way of routing the request
er := errors.InternalServerError("go.micro.api", "no route found")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.Write([]byte(er.Error()))
return
}
// create request and response
c := a.opts.Service.Client()
req := c.NewRequest(service.Name, service.Endpoint.Name, request)
rsp := &api.Response{}
// create the context from headers
cx := ctx.FromRequest(r)
// create strategy
so := selector.WithStrategy(strategy(service.Services))
if err := c.Call(cx, req, rsp, client.WithSelectOption(so)); err != nil {
w.Header().Set("Content-Type", "application/json")
ce := errors.Parse(err.Error())
switch ce.Code {
case 0:
w.WriteHeader(500)
default:
w.WriteHeader(int(ce.Code))
}
w.Write([]byte(ce.Error()))
return
} else if rsp.StatusCode == 0 {
rsp.StatusCode = http.StatusOK
}
for _, header := range rsp.GetHeader() {
for _, val := range header.Values {
w.Header().Add(header.Key, val)
}
}
if len(w.Header().Get("Content-Type")) == 0 {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(int(rsp.StatusCode))
w.Write([]byte(rsp.Body))
}
func (a *apiHandler) String() string {
return "api"
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &apiHandler{
opts: options,
}
}
func WithService(s *goapi.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &apiHandler{
opts: options,
s: s,
}
}
+107
View File
@@ -0,0 +1,107 @@
package api
import (
"fmt"
"io/ioutil"
"mime"
"net"
"net/http"
"strings"
api "github.com/micro/go-micro/api/proto"
"github.com/micro/go-micro/client/selector"
"github.com/micro/go-micro/registry"
)
func requestToProto(r *http.Request) (*api.Request, error) {
if err := r.ParseForm(); err != nil {
return nil, fmt.Errorf("Error parsing form: %v", err)
}
req := &api.Request{
Path: r.URL.Path,
Method: r.Method,
Header: make(map[string]*api.Pair),
Get: make(map[string]*api.Pair),
Post: make(map[string]*api.Pair),
Url: r.URL.String(),
}
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
ct = "application/x-www-form-urlencoded"
r.Header.Set("Content-Type", ct)
}
switch ct {
case "application/x-www-form-urlencoded":
// expect form vals
default:
data, _ := ioutil.ReadAll(r.Body)
req.Body = string(data)
}
// Set X-Forwarded-For if it does not exist
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
if prior, ok := r.Header["X-Forwarded-For"]; ok {
ip = strings.Join(prior, ", ") + ", " + ip
}
// Set the header
req.Header["X-Forwarded-For"] = &api.Pair{
Key: "X-Forwarded-For",
Values: []string{ip},
}
}
// Host is stripped from net/http Headers so let's add it
req.Header["Host"] = &api.Pair{
Key: "Host",
Values: []string{r.Host},
}
// Get data
for key, vals := range r.URL.Query() {
header, ok := req.Get[key]
if !ok {
header = &api.Pair{
Key: key,
}
req.Get[key] = header
}
header.Values = vals
}
// Post data
for key, vals := range r.PostForm {
header, ok := req.Post[key]
if !ok {
header = &api.Pair{
Key: key,
}
req.Post[key] = header
}
header.Values = vals
}
for key, vals := range r.Header {
header, ok := req.Header[key]
if !ok {
header = &api.Pair{
Key: key,
}
req.Header[key] = header
}
header.Values = vals
}
return req, nil
}
// strategy is a hack for selection
func strategy(services []*registry.Service) selector.Strategy {
return func(_ []*registry.Service) selector.Next {
// ignore input to this function, use services above
return selector.Random(services)
}
}
+46
View File
@@ -0,0 +1,46 @@
package api
import (
"net/http"
"net/url"
"testing"
)
func TestRequestToProto(t *testing.T) {
testData := []*http.Request{
&http.Request{
Method: "GET",
Header: http.Header{
"Header": []string{"test"},
},
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/foo/bar",
RawQuery: "param1=value1",
},
},
}
for _, d := range testData {
p, err := requestToProto(d)
if err != nil {
t.Fatal(err)
}
if p.Path != d.URL.Path {
t.Fatalf("Expected path %s got %s", d.URL.Path, p.Path)
}
if p.Method != d.Method {
t.Fatalf("Expected method %s got %s", d.Method, p.Method)
}
for k, v := range d.Header {
if val, ok := p.Header[k]; !ok {
t.Fatalf("Expected header %s", k)
} else {
if val.Values[0] != v[0] {
t.Fatalf("Expected val %s, got %s", val.Values[0], v[0])
}
}
}
}
}
+268
View File
@@ -0,0 +1,268 @@
// Package broker provides a go-micro/broker handler
package broker
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/micro/go-micro/api/handler"
"github.com/micro/go-micro/broker"
"github.com/micro/go-micro/util/log"
)
const (
Handler = "broker"
pingTime = (readDeadline * 9) / 10
readLimit = 16384
readDeadline = 60 * time.Second
writeDeadline = 10 * time.Second
)
type brokerHandler struct {
opts handler.Options
u websocket.Upgrader
}
type conn struct {
b broker.Broker
cType string
topic string
queue string
exit chan bool
sync.Mutex
ws *websocket.Conn
}
var (
once sync.Once
contentType = "text/plain"
)
func checkOrigin(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true
}
u, err := url.Parse(origin[0])
if err != nil {
return false
}
return u.Host == r.Host
}
func (c *conn) close() {
select {
case <-c.exit:
return
default:
close(c.exit)
}
}
func (c *conn) readLoop() {
defer func() {
c.close()
c.ws.Close()
}()
// set read limit/deadline
c.ws.SetReadLimit(readLimit)
c.ws.SetReadDeadline(time.Now().Add(readDeadline))
// set close handler
ch := c.ws.CloseHandler()
c.ws.SetCloseHandler(func(code int, text string) error {
err := ch(code, text)
c.close()
return err
})
// set pong handler
c.ws.SetPongHandler(func(string) error {
c.ws.SetReadDeadline(time.Now().Add(readDeadline))
return nil
})
for {
_, message, err := c.ws.ReadMessage()
if err != nil {
return
}
c.b.Publish(c.topic, &broker.Message{
Header: map[string]string{"Content-Type": c.cType},
Body: message,
})
}
}
func (c *conn) write(mType int, data []byte) error {
c.Lock()
c.ws.SetWriteDeadline(time.Now().Add(writeDeadline))
err := c.ws.WriteMessage(mType, data)
c.Unlock()
return err
}
func (c *conn) writeLoop() {
ticker := time.NewTicker(pingTime)
var opts []broker.SubscribeOption
if len(c.queue) > 0 {
opts = append(opts, broker.Queue(c.queue))
}
subscriber, err := c.b.Subscribe(c.topic, func(p broker.Event) error {
b, err := json.Marshal(p.Message())
if err != nil {
return nil
}
return c.write(websocket.TextMessage, b)
}, opts...)
defer func() {
subscriber.Unsubscribe()
ticker.Stop()
c.ws.Close()
}()
if err != nil {
log.Log(err.Error())
return
}
for {
select {
case <-ticker.C:
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
return
}
case <-c.exit:
return
}
}
}
func (b *brokerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
br := b.opts.Service.Client().Options().Broker
// Setup the broker
once.Do(func() {
br.Init()
br.Connect()
})
// Parse
r.ParseForm()
topic := r.Form.Get("topic")
// Can't do anything without a topic
if len(topic) == 0 {
http.Error(w, "Topic not specified", 400)
return
}
// Post assumed to be Publish
if r.Method == "POST" {
// Create a broker message
msg := &broker.Message{
Header: make(map[string]string),
}
// Set header
for k, v := range r.Header {
msg.Header[k] = strings.Join(v, ", ")
}
// Read body
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// Set body
msg.Body = b
// Publish
br.Publish(topic, msg)
return
}
// now back to our regularly scheduled programming
if r.Method != "GET" {
http.Error(w, "Method not allowed", 405)
return
}
queue := r.Form.Get("queue")
ws, err := b.u.Upgrade(w, r, nil)
if err != nil {
log.Log(err.Error())
return
}
cType := r.Header.Get("Content-Type")
if len(cType) == 0 {
cType = contentType
}
c := &conn{
b: br,
cType: cType,
topic: topic,
queue: queue,
exit: make(chan bool),
ws: ws,
}
go c.writeLoop()
c.readLoop()
}
func (b *brokerHandler) String() string {
return "broker"
}
func NewHandler(opts ...handler.Option) handler.Handler {
return &brokerHandler{
u: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
opts: handler.NewOptions(opts...),
}
}
func WithCors(cors map[string]bool, opts ...handler.Option) handler.Handler {
return &brokerHandler{
u: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if origin := r.Header.Get("Origin"); cors[origin] {
return true
} else if len(origin) > 0 && cors["*"] {
return true
} else if checkOrigin(r) {
return true
}
return false
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
opts: handler.NewOptions(opts...),
}
}
+94
View File
@@ -0,0 +1,94 @@
// Package cloudevents provides a cloudevents handler publishing the event using the go-micro/client
package cloudevents
import (
"net/http"
"path"
"regexp"
"strings"
"github.com/micro/go-micro/api/handler"
"github.com/micro/go-micro/util/ctx"
)
type event struct {
options handler.Options
}
var (
Handler = "cloudevents"
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
)
func eventName(parts []string) string {
return strings.Join(parts, ".")
}
func evRoute(ns, p string) (string, string) {
p = path.Clean(p)
p = strings.TrimPrefix(p, "/")
if len(p) == 0 {
return ns, "event"
}
parts := strings.Split(p, "/")
// no path
if len(parts) == 0 {
// topic: namespace
// action: event
return strings.Trim(ns, "."), "event"
}
// Treat /v[0-9]+ as versioning
// /v1/foo/bar => topic: v1.foo action: bar
if len(parts) >= 2 && versionRe.Match([]byte(parts[0])) {
topic := ns + "." + strings.Join(parts[:2], ".")
action := eventName(parts[1:])
return topic, action
}
// /foo => topic: ns.foo action: foo
// /foo/bar => topic: ns.foo action: bar
topic := ns + "." + strings.Join(parts[:1], ".")
action := eventName(parts[1:])
return topic, action
}
func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// request to topic:event
// create event
// publish to topic
topic, _ := evRoute(e.options.Namespace, r.URL.Path)
// create event
ev, err := FromRequest(r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// get client
c := e.options.Service.Client()
// create publication
p := c.NewMessage(topic, ev)
// publish event
if err := c.Publish(ctx.FromRequest(r), p); err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (e *event) String() string {
return "cloudevents"
}
func NewHandler(opts ...handler.Option) handler.Handler {
return &event{
options: handler.NewOptions(opts...),
}
}
+282
View File
@@ -0,0 +1,282 @@
/*
* From: https://github.com/serverless/event-gateway/blob/master/event/event.go
* Modified: Strip to handler requirements
*
* Copyright 2017 Serverless, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cloudevents
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"mime"
"net/http"
"strings"
"time"
"unicode"
"github.com/google/uuid"
"gopkg.in/go-playground/validator.v9"
)
const (
// TransformationVersion is indicative of the revision of how Event Gateway transforms a request into CloudEvents format.
TransformationVersion = "0.1"
// CloudEventsVersion currently supported by Event Gateway
CloudEventsVersion = "0.1"
)
// Event is a default event structure. All data that passes through the Event Gateway
// is formatted to a format defined CloudEvents v0.1 spec.
type Event struct {
EventType string `json:"eventType" validate:"required"`
EventTypeVersion string `json:"eventTypeVersion,omitempty"`
CloudEventsVersion string `json:"cloudEventsVersion" validate:"required"`
Source string `json:"source" validate:"uri,required"`
EventID string `json:"eventID" validate:"required"`
EventTime *time.Time `json:"eventTime,omitempty"`
SchemaURL string `json:"schemaURL,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
ContentType string `json:"contentType,omitempty"`
Data interface{} `json:"data"`
}
// New return new instance of Event.
func New(eventType string, mimeType string, payload interface{}) *Event {
now := time.Now()
event := &Event{
EventType: eventType,
CloudEventsVersion: CloudEventsVersion,
Source: "https://micro.mu",
EventID: uuid.New().String(),
EventTime: &now,
ContentType: mimeType,
Data: payload,
Extensions: map[string]interface{}{
"eventgateway": map[string]interface{}{
"transformed": "true",
"transformation-version": TransformationVersion,
},
},
}
event.Data = normalizePayload(event.Data, event.ContentType)
return event
}
// FromRequest takes an HTTP request and returns an Event along with path. Most of the implementation
// is based on https://github.com/cloudevents/spec/blob/master/http-transport-binding.md.
// This function also supports legacy mode where event type is sent in Event header.
func FromRequest(r *http.Request) (*Event, error) {
contentType := r.Header.Get("Content-Type")
mimeType, _, err := mime.ParseMediaType(contentType)
if err != nil {
if err.Error() != "mime: no media type" {
return nil, err
}
mimeType = "application/octet-stream"
}
// Read request body
body := []byte{}
if r.Body != nil {
body, err = ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
}
var event *Event
if mimeType == mimeCloudEventsJSON { // CloudEvents Structured Content Mode
return parseAsCloudEvent(mimeType, body)
} else if isCloudEventsBinaryContentMode(r.Header) { // CloudEvents Binary Content Mode
return parseAsCloudEventBinary(r.Header, body)
} else if isLegacyMode(r.Header) {
if mimeType == mimeJSON { // CloudEvent in Legacy Mode
event, err = parseAsCloudEvent(mimeType, body)
if err != nil {
return New(string(r.Header.Get("event")), mimeType, body), nil
}
return event, err
}
return New(string(r.Header.Get("event")), mimeType, body), nil
}
return New("http.request", mimeJSON, newHTTPRequestData(r, body)), nil
}
// Validate Event struct
func (e *Event) Validate() error {
validate := validator.New()
err := validate.Struct(e)
if err != nil {
return fmt.Errorf("CloudEvent not valid: %v", err)
}
return nil
}
func isLegacyMode(headers http.Header) bool {
if headers.Get("Event") != "" {
return true
}
return false
}
func isCloudEventsBinaryContentMode(headers http.Header) bool {
if headers.Get("CE-EventType") != "" &&
headers.Get("CE-CloudEventsVersion") != "" &&
headers.Get("CE-Source") != "" &&
headers.Get("CE-EventID") != "" {
return true
}
return false
}
func parseAsCloudEventBinary(headers http.Header, payload interface{}) (*Event, error) {
event := &Event{
EventType: headers.Get("CE-EventType"),
EventTypeVersion: headers.Get("CE-EventTypeVersion"),
CloudEventsVersion: headers.Get("CE-CloudEventsVersion"),
Source: headers.Get("CE-Source"),
EventID: headers.Get("CE-EventID"),
ContentType: headers.Get("Content-Type"),
Data: payload,
}
err := event.Validate()
if err != nil {
return nil, err
}
if headers.Get("CE-EventTime") != "" {
val, err := time.Parse(time.RFC3339, headers.Get("CE-EventTime"))
if err != nil {
return nil, err
}
event.EventTime = &val
}
if val := headers.Get("CE-SchemaURL"); len(val) > 0 {
event.SchemaURL = val
}
event.Extensions = map[string]interface{}{}
for key, val := range flatten(headers) {
if strings.HasPrefix(key, "Ce-X-") {
key = strings.TrimLeft(key, "Ce-X-")
// Make first character lowercase
runes := []rune(key)
runes[0] = unicode.ToLower(runes[0])
event.Extensions[string(runes)] = val
}
}
event.Data = normalizePayload(event.Data, event.ContentType)
return event, nil
}
func flatten(h http.Header) map[string]string {
headers := map[string]string{}
for key, header := range h {
headers[key] = header[0]
if len(header) > 1 {
headers[key] = strings.Join(header, ", ")
}
}
return headers
}
func parseAsCloudEvent(mime string, payload interface{}) (*Event, error) {
body, ok := payload.([]byte)
if ok {
event := &Event{}
err := json.Unmarshal(body, event)
if err != nil {
return nil, err
}
err = event.Validate()
if err != nil {
return nil, err
}
event.Data = normalizePayload(event.Data, event.ContentType)
return event, nil
}
return nil, errors.New("couldn't cast to []byte")
}
const (
mimeJSON = "application/json"
mimeFormMultipart = "multipart/form-data"
mimeFormURLEncoded = "application/x-www-form-urlencoded"
mimeCloudEventsJSON = "application/cloudevents+json"
)
// normalizePayload takes anything, checks if it's []byte array and depending on provided mime
// type converts it to either string or map[string]interface to avoid having base64 string after
// JSON marshaling.
func normalizePayload(payload interface{}, mime string) interface{} {
if bytePayload, ok := payload.([]byte); ok && len(bytePayload) > 0 {
switch {
case mime == mimeJSON || strings.HasSuffix(mime, "+json"):
var result map[string]interface{}
err := json.Unmarshal(bytePayload, &result)
if err != nil {
return payload
}
return result
case strings.HasPrefix(mime, mimeFormMultipart), mime == mimeFormURLEncoded:
return string(bytePayload)
}
}
return payload
}
// HTTPRequestData is a event schema used for sending events to HTTP subscriptions.
type HTTPRequestData struct {
Headers map[string]string `json:"headers"`
Query map[string][]string `json:"query"`
Body interface{} `json:"body"`
Host string `json:"host"`
Path string `json:"path"`
Method string `json:"method"`
Params map[string]string `json:"params"`
}
// NewHTTPRequestData returns a new instance of HTTPRequestData
func newHTTPRequestData(r *http.Request, eventData interface{}) *HTTPRequestData {
req := &HTTPRequestData{
Headers: flatten(r.Header),
Query: r.URL.Query(),
Body: eventData,
Host: r.Host,
Path: r.URL.Path,
Method: r.Method,
}
req.Body = normalizePayload(req.Body, r.Header.Get("content-type"))
return req
}
+122
View File
@@ -0,0 +1,122 @@
// Package event provides a handler which publishes an event
package event
import (
"fmt"
"io/ioutil"
"net/http"
"path"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/micro/go-micro/api/handler"
proto "github.com/micro/go-micro/api/proto"
"github.com/micro/go-micro/util/ctx"
)
type event struct {
options handler.Options
}
var (
Handler = "event"
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
)
func eventName(parts []string) string {
return strings.Join(parts, ".")
}
func evRoute(ns, p string) (string, string) {
p = path.Clean(p)
p = strings.TrimPrefix(p, "/")
if len(p) == 0 {
return ns, "event"
}
parts := strings.Split(p, "/")
// no path
if len(parts) == 0 {
// topic: namespace
// action: event
return strings.Trim(ns, "."), "event"
}
// Treat /v[0-9]+ as versioning
// /v1/foo/bar => topic: v1.foo action: bar
if len(parts) >= 2 && versionRe.Match([]byte(parts[0])) {
topic := ns + "." + strings.Join(parts[:2], ".")
action := eventName(parts[1:])
return topic, action
}
// /foo => topic: ns.foo action: foo
// /foo/bar => topic: ns.foo action: bar
topic := ns + "." + strings.Join(parts[:1], ".")
action := eventName(parts[1:])
return topic, action
}
func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// request to topic:event
// create event
// publish to topic
topic, action := evRoute(e.options.Namespace, r.URL.Path)
// create event
ev := &proto.Event{
Name: action,
// TODO: dedupe event
Id: fmt.Sprintf("%s-%s-%s", topic, action, uuid.New().String()),
Header: make(map[string]*proto.Pair),
Timestamp: time.Now().Unix(),
}
// set headers
for key, vals := range r.Header {
header, ok := ev.Header[key]
if !ok {
header = &proto.Pair{
Key: key,
}
ev.Header[key] = header
}
header.Values = vals
}
// set body
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
ev.Data = string(b)
// get client
c := e.options.Service.Client()
// create publication
p := c.NewMessage(topic, ev)
// publish event
if err := c.Publish(ctx.FromRequest(r), p); err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (e *event) String() string {
return "event"
}
func NewHandler(opts ...handler.Option) handler.Handler {
return &event{
options: handler.NewOptions(opts...),
}
}
+16
View File
@@ -0,0 +1,16 @@
// Package file serves file relative to the current directory
package file
import (
"net/http"
)
type Handler struct{}
func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "."+r.URL.Path)
}
func (h *Handler) String() string {
return "file"
}
+14
View File
@@ -0,0 +1,14 @@
// Package handler provides http handlers
package handler
import (
"net/http"
)
// Handler represents a HTTP handler that manages a request
type Handler interface {
// standard http handler
http.Handler
// name of handler
String() string
}
+100
View File
@@ -0,0 +1,100 @@
// Package http is a http reverse proxy handler
package http
import (
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"github.com/micro/go-micro/api"
"github.com/micro/go-micro/api/handler"
"github.com/micro/go-micro/client/selector"
)
const (
Handler = "http"
)
type httpHandler struct {
options handler.Options
// set with different initialiser
s *api.Service
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
service, err := h.getService(r)
if err != nil {
w.WriteHeader(500)
return
}
if len(service) == 0 {
w.WriteHeader(404)
return
}
rp, err := url.Parse(service)
if err != nil {
w.WriteHeader(500)
return
}
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
}
// getService returns the service for this request from the selector
func (h *httpHandler) getService(r *http.Request) (string, error) {
var service *api.Service
if h.s != nil {
// we were given the service
service = h.s
} else if h.options.Router != nil {
// try get service from router
s, err := h.options.Router.Route(r)
if err != nil {
return "", err
}
service = s
} else {
// we have no way of routing the request
return "", errors.New("no route found")
}
// create a random selector
next := selector.Random(service.Services)
// get the next node
s, err := next()
if err != nil {
return "", nil
}
return fmt.Sprintf("http://%s", s.Address), nil
}
func (h *httpHandler) String() string {
return "http"
}
// NewHandler returns a http proxy handler
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &httpHandler{
options: options,
}
}
// WithService creates a handler with a service
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &httpHandler{
options: options,
s: s,
}
}
+122
View File
@@ -0,0 +1,122 @@
package http
import (
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/micro/go-micro/api/handler"
"github.com/micro/go-micro/api/router"
regRouter "github.com/micro/go-micro/api/router/registry"
"github.com/micro/go-micro/config/cmd"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/memory"
)
func testHttp(t *testing.T, path, service, ns string) {
r := memory.NewRegistry()
cmd.DefaultCmd = cmd.NewCmd(cmd.Registry(&r))
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
s := &registry.Service{
Name: service,
Nodes: []*registry.Node{
&registry.Node{
Id: service + "-1",
Address: l.Addr().String(),
},
},
}
r.Register(s)
defer r.Deregister(s)
// setup the test handler
m := http.NewServeMux()
m.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`you got served`))
})
// start http test serve
go http.Serve(l, m)
// create new request and writer
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", path, nil)
if err != nil {
t.Fatal(err)
}
// initialise the handler
rt := regRouter.NewRouter(
router.WithHandler("http"),
router.WithNamespace(ns),
)
p := NewHandler(handler.WithRouter(rt))
// execute the handler
p.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("Expected 200 response got %d %s", w.Code, w.Body.String())
}
if w.Body.String() != "you got served" {
t.Fatalf("Expected body: you got served. Got: %s", w.Body.String())
}
}
func TestHttpHandler(t *testing.T) {
testData := []struct {
path string
service string
namespace string
}{
{
"/test/foo",
"go.micro.api.test",
"go.micro.api",
},
{
"/test/foo/baz",
"go.micro.api.test",
"go.micro.api",
},
{
"/v1/foo",
"go.micro.api.v1.foo",
"go.micro.api",
},
{
"/v1/foo/bar",
"go.micro.api.v1.foo",
"go.micro.api",
},
{
"/v2/baz",
"go.micro.api.v2.baz",
"go.micro.api",
},
{
"/v2/baz/bar",
"go.micro.api.v2.baz",
"go.micro.api",
},
{
"/v2/baz/bar",
"v2.baz",
"",
},
}
for _, d := range testData {
testHttp(t, d.path, d.service, d.namespace)
}
}
+55
View File
@@ -0,0 +1,55 @@
package handler
import (
"github.com/micro/go-micro"
"github.com/micro/go-micro/api/router"
)
type Options struct {
Namespace string
Router router.Router
Service micro.Service
}
type Option func(o *Options)
// NewOptions fills in the blanks
func NewOptions(opts ...Option) Options {
var options Options
for _, o := range opts {
o(&options)
}
// create service if its blank
if options.Service == nil {
WithService(micro.NewService())(&options)
}
// set namespace if blank
if len(options.Namespace) == 0 {
WithNamespace("go.micro.api")(&options)
}
return options
}
// WithNamespace specifies the namespace for the handler
func WithNamespace(s string) Option {
return func(o *Options) {
o.Namespace = s
}
}
// WithRouter specifies a router to be used by the handler
func WithRouter(r router.Router) Option {
return func(o *Options) {
o.Router = r
}
}
// WithService specifies a micro.Service
func WithService(s micro.Service) Option {
return func(o *Options) {
o.Service = s
}
}
+211
View File
@@ -0,0 +1,211 @@
// Package registry is a go-micro/registry handler
package registry
import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/gorilla/websocket"
"github.com/micro/go-micro/api/handler"
"github.com/micro/go-micro/registry"
)
const (
Handler = "registry"
pingTime = (readDeadline * 9) / 10
readLimit = 16384
readDeadline = 60 * time.Second
writeDeadline = 10 * time.Second
)
type registryHandler struct {
opts handler.Options
reg registry.Registry
}
func (rh *registryHandler) add(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer r.Body.Close()
var opts []registry.RegisterOption
// parse ttl
if ttl := r.Form.Get("ttl"); len(ttl) > 0 {
d, err := time.ParseDuration(ttl)
if err == nil {
opts = append(opts, registry.RegisterTTL(d))
}
}
var service *registry.Service
err = json.Unmarshal(b, &service)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = rh.reg.Register(service, opts...)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (rh *registryHandler) del(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer r.Body.Close()
var service *registry.Service
err = json.Unmarshal(b, &service)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = rh.reg.Deregister(service)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (rh *registryHandler) get(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
service := r.Form.Get("service")
var s []*registry.Service
var err error
if len(service) == 0 {
//
upgrade := r.Header.Get("Upgrade")
connect := r.Header.Get("Connection")
// watch if websockets
if upgrade == "websocket" && connect == "Upgrade" {
rw, err := rh.reg.Watch()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
watch(rw, w, r)
return
}
// otherwise list services
s, err = rh.reg.ListServices()
} else {
s, err = rh.reg.GetService(service)
}
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if s == nil || (len(service) > 0 && (len(s) == 0 || len(s[0].Name) == 0)) {
http.Error(w, "Service not found", 404)
return
}
b, err := json.Marshal(s)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
w.Write(b)
}
func ping(ws *websocket.Conn, exit chan bool) {
ticker := time.NewTicker(pingTime)
for {
select {
case <-ticker.C:
ws.SetWriteDeadline(time.Now().Add(writeDeadline))
err := ws.WriteMessage(websocket.PingMessage, []byte{})
if err != nil {
return
}
case <-exit:
return
}
}
}
func watch(rw registry.Watcher, w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// we need an exit chan
exit := make(chan bool)
defer func() {
close(exit)
}()
// ping the socket
go ping(ws, exit)
for {
// get next result
r, err := rw.Next()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// write to client
ws.SetWriteDeadline(time.Now().Add(writeDeadline))
if err := ws.WriteJSON(r); err != nil {
return
}
}
}
func (rh *registryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
rh.get(w, r)
case "POST":
rh.add(w, r)
case "DELETE":
rh.del(w, r)
}
}
func (rh *registryHandler) String() string {
return "registry"
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &registryHandler{
opts: options,
reg: options.Service.Client().Options().Registry,
}
}
+309
View File
@@ -0,0 +1,309 @@
// Package rpc is a go-micro rpc handler.
package rpc
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
"github.com/joncalhoun/qson"
"github.com/micro/go-micro/api"
"github.com/micro/go-micro/api/handler"
proto "github.com/micro/go-micro/api/internal/proto"
"github.com/micro/go-micro/client"
"github.com/micro/go-micro/client/selector"
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/codec/jsonrpc"
"github.com/micro/go-micro/codec/protorpc"
"github.com/micro/go-micro/errors"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/util/ctx"
)
const (
Handler = "rpc"
)
var (
// supported json codecs
jsonCodecs = []string{
"application/grpc+json",
"application/json",
"application/json-rpc",
}
// support proto codecs
protoCodecs = []string{
"application/grpc",
"application/grpc+proto",
"application/proto",
"application/protobuf",
"application/proto-rpc",
"application/octet-stream",
}
)
type rpcHandler struct {
opts handler.Options
s *api.Service
}
type buffer struct {
io.ReadCloser
}
func (b *buffer) Write(_ []byte) (int, error) {
return 0, nil
}
// strategy is a hack for selection
func strategy(services []*registry.Service) selector.Strategy {
return func(_ []*registry.Service) selector.Next {
// ignore input to this function, use services above
return selector.Random(services)
}
}
func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var service *api.Service
if h.s != nil {
// we were given the service
service = h.s
} else if h.opts.Router != nil {
// try get service from router
s, err := h.opts.Router.Route(r)
if err != nil {
writeError(w, r, errors.InternalServerError("go.micro.api", err.Error()))
return
}
service = s
} else {
// we have no way of routing the request
writeError(w, r, errors.InternalServerError("go.micro.api", "no route found"))
return
}
// only allow post when we have the router
if r.Method != "GET" && (h.opts.Router != nil && r.Method != "POST") {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ct := r.Header.Get("Content-Type")
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
ct = ct[:idx]
}
// micro client
c := h.opts.Service.Client()
// create strategy
so := selector.WithStrategy(strategy(service.Services))
// get payload
br, err := requestPayload(r)
if err != nil {
writeError(w, r, err)
return
}
// create context
cx := ctx.FromRequest(r)
var rsp []byte
switch {
// proto codecs
case hasCodec(ct, protoCodecs):
request := &proto.Message{}
// if the extracted payload isn't empty lets use it
if len(br) > 0 {
request = proto.NewMessage(br)
}
// create request/response
response := &proto.Message{}
req := c.NewRequest(
service.Name,
service.Endpoint.Name,
request,
client.WithContentType(ct),
)
// make the call
if err := c.Call(cx, req, response, client.WithSelectOption(so)); err != nil {
writeError(w, r, err)
return
}
// marshall response
rsp, _ = response.Marshal()
default:
// if json codec is not present set to json
if !hasCodec(ct, jsonCodecs) {
ct = "application/json"
}
// default to trying json
var request json.RawMessage
// if the extracted payload isn't empty lets use it
if len(br) > 0 {
request = json.RawMessage(br)
}
// create request/response
var response json.RawMessage
req := c.NewRequest(
service.Name,
service.Endpoint.Name,
&request,
client.WithContentType(ct),
)
// make the call
if err := c.Call(cx, req, &response, client.WithSelectOption(so)); err != nil {
writeError(w, r, err)
return
}
// marshall response
rsp, _ = response.MarshalJSON()
}
// write the response
writeResponse(w, r, rsp)
}
func (rh *rpcHandler) String() string {
return "rpc"
}
func hasCodec(ct string, codecs []string) bool {
for _, codec := range codecs {
if ct == codec {
return true
}
}
return false
}
// requestPayload takes a *http.Request.
// If the request is a GET the query string parameters are extracted and marshaled to JSON and the raw bytes are returned.
// If the request method is a POST the request body is read and returned
func requestPayload(r *http.Request) ([]byte, error) {
// we have to decode json-rpc and proto-rpc because we suck
// well actually because there's no proxy codec right now
switch r.Header.Get("Content-Type") {
case "application/json-rpc":
msg := codec.Message{
Type: codec.Request,
Header: make(map[string]string),
}
c := jsonrpc.NewCodec(&buffer{r.Body})
if err := c.ReadHeader(&msg, codec.Request); err != nil {
return nil, err
}
var raw json.RawMessage
if err := c.ReadBody(&raw); err != nil {
return nil, err
}
return ([]byte)(raw), nil
case "application/proto-rpc", "application/octet-stream":
msg := codec.Message{
Type: codec.Request,
Header: make(map[string]string),
}
c := protorpc.NewCodec(&buffer{r.Body})
if err := c.ReadHeader(&msg, codec.Request); err != nil {
return nil, err
}
var raw proto.Message
if err := c.ReadBody(&raw); err != nil {
return nil, err
}
b, _ := raw.Marshal()
return b, nil
}
// otherwise as per usual
switch r.Method {
case "GET":
if len(r.URL.RawQuery) > 0 {
return qson.ToJSON(r.URL.RawQuery)
}
case "PATCH", "POST":
return ioutil.ReadAll(r.Body)
}
return []byte{}, nil
}
func writeError(w http.ResponseWriter, r *http.Request, err error) {
ce := errors.Parse(err.Error())
switch ce.Code {
case 0:
// assuming it's totally screwed
ce.Code = 500
ce.Id = "go.micro.api"
ce.Status = http.StatusText(500)
ce.Detail = "error during request: " + ce.Detail
w.WriteHeader(500)
default:
w.WriteHeader(int(ce.Code))
}
// response content type
w.Header().Set("Content-Type", "application/json")
// Set trailers
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
w.Header().Set("Trailer", "grpc-status")
w.Header().Set("Trailer", "grpc-message")
w.Header().Set("grpc-status", "13")
w.Header().Set("grpc-message", ce.Detail)
}
w.Write([]byte(ce.Error()))
}
func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) {
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
w.Header().Set("Content-Length", strconv.Itoa(len(rsp)))
// Set trailers
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
w.Header().Set("Trailer", "grpc-status")
w.Header().Set("Trailer", "grpc-message")
w.Header().Set("grpc-status", "0")
w.Header().Set("grpc-message", "")
}
// write response
w.Write(rsp)
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &rpcHandler{
opts: options,
}
}
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &rpcHandler{
opts: options,
s: s,
}
}
+95
View File
@@ -0,0 +1,95 @@
package rpc
import (
"bytes"
"encoding/json"
"net/http"
"testing"
"github.com/golang/protobuf/proto"
"github.com/micro/go-micro/api/proto"
)
func TestRequestPayloadFromRequest(t *testing.T) {
// our test event so that we can validate serialising / deserializing of true protos works
protoEvent := go_api.Event{
Name: "Test",
}
protoBytes, err := proto.Marshal(&protoEvent)
if err != nil {
t.Fatal("Failed to marshal proto", err)
}
jsonBytes, err := json.Marshal(protoEvent)
if err != nil {
t.Fatal("Failed to marshal proto to JSON ", err)
}
t.Run("extracting a proto from a POST request", func(t *testing.T) {
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(protoBytes))
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != string(protoBytes) {
t.Fatalf("Expected %v and %v to match", string(extByte), string(protoBytes))
}
})
t.Run("extracting JSON from a POST request", func(t *testing.T) {
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(jsonBytes))
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != string(jsonBytes) {
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
}
})
t.Run("extracting params from a GET request", func(t *testing.T) {
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
q := r.URL.Query()
q.Add("name", "Test")
r.URL.RawQuery = q.Encode()
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != string(jsonBytes) {
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
}
})
t.Run("GET request with no params", func(t *testing.T) {
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != "" {
t.Fatalf("Expected %v and %v to match", string(extByte), "")
}
})
}
+25
View File
@@ -0,0 +1,25 @@
// Package udp reads and write from a udp connection
package udp
import (
"io"
"net"
"net/http"
)
type Handler struct{}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c, err := net.Dial("udp", r.Host)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
go io.Copy(c, r.Body)
// write response
io.Copy(w, c)
}
func (h *Handler) String() string {
return "udp"
}
+30
View File
@@ -0,0 +1,30 @@
// Package unix reads from a unix socket expecting it to be in /tmp/path
package unix
import (
"fmt"
"io"
"net"
"net/http"
"path/filepath"
)
type Handler struct{}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sock := fmt.Sprintf("%s.sock", filepath.Clean(r.URL.Path))
path := filepath.Join("/tmp", sock)
c, err := net.Dial("unix", path)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
go io.Copy(c, r.Body)
// write response
io.Copy(w, c)
}
func (h *Handler) String() string {
return "unix"
}
+177
View File
@@ -0,0 +1,177 @@
// Package web contains the web handler including websocket support
package web
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/micro/go-micro/api"
"github.com/micro/go-micro/api/handler"
"github.com/micro/go-micro/client/selector"
)
const (
Handler = "web"
)
type webHandler struct {
opts handler.Options
s *api.Service
}
func (wh *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
service, err := wh.getService(r)
if err != nil {
w.WriteHeader(500)
return
}
if len(service) == 0 {
w.WriteHeader(404)
return
}
rp, err := url.Parse(service)
if err != nil {
w.WriteHeader(500)
return
}
if isWebSocket(r) {
wh.serveWebSocket(rp.Host, w, r)
return
}
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
}
// getService returns the service for this request from the selector
func (wh *webHandler) getService(r *http.Request) (string, error) {
var service *api.Service
if wh.s != nil {
// we were given the service
service = wh.s
} else if wh.opts.Router != nil {
// try get service from router
s, err := wh.opts.Router.Route(r)
if err != nil {
return "", err
}
service = s
} else {
// we have no way of routing the request
return "", errors.New("no route found")
}
// create a random selector
next := selector.Random(service.Services)
// get the next node
s, err := next()
if err != nil {
return "", nil
}
return fmt.Sprintf("http://%s", s.Address), nil
}
// serveWebSocket used to serve a web socket proxied connection
func (wh *webHandler) serveWebSocket(host string, w http.ResponseWriter, r *http.Request) {
req := new(http.Request)
*req = *r
if len(host) == 0 {
http.Error(w, "invalid host", 500)
return
}
// set x-forward-for
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
if ips, ok := req.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(ips, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
}
// connect to the backend host
conn, err := net.Dial("tcp", host)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// hijack the connection
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "failed to connect", 500)
return
}
nc, _, err := hj.Hijack()
if err != nil {
return
}
defer nc.Close()
defer conn.Close()
if err = req.Write(conn); err != nil {
return
}
errCh := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errCh <- err
}
go cp(conn, nc)
go cp(nc, conn)
<-errCh
}
func isWebSocket(r *http.Request) bool {
contains := func(key, val string) bool {
vv := strings.Split(r.Header.Get(key), ",")
for _, v := range vv {
if val == strings.ToLower(strings.TrimSpace(v)) {
return true
}
}
return false
}
if contains("Connection", "upgrade") && contains("Upgrade", "websocket") {
return true
}
return false
}
func (wh *webHandler) String() string {
return "web"
}
func NewHandler(opts ...handler.Option) handler.Handler {
return &webHandler{
opts: handler.NewOptions(opts...),
}
}
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &webHandler{
opts: options,
s: s,
}
}
@@ -1,16 +1,7 @@
package proto
type Message struct {
Data []byte
}
func (m *Message) MarshalJSON() ([]byte, error) {
return m.Data, nil
}
func (m *Message) UnmarshalJSON(data []byte) error {
m.Data = data
return nil
data []byte
}
func (m *Message) ProtoMessage() {}
@@ -20,15 +11,15 @@ func (m *Message) Reset() {
}
func (m *Message) String() string {
return string(m.Data)
return string(m.data)
}
func (m *Message) Marshal() ([]byte, error) {
return m.Data, nil
return m.data, nil
}
func (m *Message) Unmarshal(data []byte) error {
m.Data = data
m.data = data
return nil
}
@@ -1,13 +1,23 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: codec/protorpc/envelope.proto
// source: github.com/micro/go-micro/api/proto/api.proto
package protorpc
/*
Package go_api is a generated protocol buffer package.
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
It is generated from these files:
github.com/micro/go-micro/api/proto/api.proto
It has these top-level messages:
Pair
Request
Response
Event
*/
package go_api
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
@@ -18,4 +28,4 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
+332
View File
@@ -0,0 +1,332 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/micro/go-micro/api/proto/api.proto
package go_api
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Pair struct {
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Pair) Reset() { *m = Pair{} }
func (m *Pair) String() string { return proto.CompactTextString(m) }
func (*Pair) ProtoMessage() {}
func (*Pair) Descriptor() ([]byte, []int) {
return fileDescriptor_api_17a7876430d97ebd, []int{0}
}
func (m *Pair) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Pair.Unmarshal(m, b)
}
func (m *Pair) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Pair.Marshal(b, m, deterministic)
}
func (dst *Pair) XXX_Merge(src proto.Message) {
xxx_messageInfo_Pair.Merge(dst, src)
}
func (m *Pair) XXX_Size() int {
return xxx_messageInfo_Pair.Size(m)
}
func (m *Pair) XXX_DiscardUnknown() {
xxx_messageInfo_Pair.DiscardUnknown(m)
}
var xxx_messageInfo_Pair proto.InternalMessageInfo
func (m *Pair) GetKey() string {
if m != nil {
return m.Key
}
return ""
}
func (m *Pair) GetValues() []string {
if m != nil {
return m.Values
}
return nil
}
// A HTTP request as RPC
// Forward by the api handler
type Request struct {
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"`
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
Header map[string]*Pair `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Get map[string]*Pair `protobuf:"bytes,4,rep,name=get,proto3" json:"get,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Post map[string]*Pair `protobuf:"bytes,5,rep,name=post,proto3" json:"post,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
Url string `protobuf:"bytes,7,opt,name=url,proto3" json:"url,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Request) Reset() { *m = Request{} }
func (m *Request) String() string { return proto.CompactTextString(m) }
func (*Request) ProtoMessage() {}
func (*Request) Descriptor() ([]byte, []int) {
return fileDescriptor_api_17a7876430d97ebd, []int{1}
}
func (m *Request) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Request.Unmarshal(m, b)
}
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
}
func (dst *Request) XXX_Merge(src proto.Message) {
xxx_messageInfo_Request.Merge(dst, src)
}
func (m *Request) XXX_Size() int {
return xxx_messageInfo_Request.Size(m)
}
func (m *Request) XXX_DiscardUnknown() {
xxx_messageInfo_Request.DiscardUnknown(m)
}
var xxx_messageInfo_Request proto.InternalMessageInfo
func (m *Request) GetMethod() string {
if m != nil {
return m.Method
}
return ""
}
func (m *Request) GetPath() string {
if m != nil {
return m.Path
}
return ""
}
func (m *Request) GetHeader() map[string]*Pair {
if m != nil {
return m.Header
}
return nil
}
func (m *Request) GetGet() map[string]*Pair {
if m != nil {
return m.Get
}
return nil
}
func (m *Request) GetPost() map[string]*Pair {
if m != nil {
return m.Post
}
return nil
}
func (m *Request) GetBody() string {
if m != nil {
return m.Body
}
return ""
}
func (m *Request) GetUrl() string {
if m != nil {
return m.Url
}
return ""
}
// A HTTP response as RPC
// Expected response for the api handler
type Response struct {
StatusCode int32 `protobuf:"varint,1,opt,name=statusCode,proto3" json:"statusCode,omitempty"`
Header map[string]*Pair `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return proto.CompactTextString(m) }
func (*Response) ProtoMessage() {}
func (*Response) Descriptor() ([]byte, []int) {
return fileDescriptor_api_17a7876430d97ebd, []int{2}
}
func (m *Response) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Response.Unmarshal(m, b)
}
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
}
func (dst *Response) XXX_Merge(src proto.Message) {
xxx_messageInfo_Response.Merge(dst, src)
}
func (m *Response) XXX_Size() int {
return xxx_messageInfo_Response.Size(m)
}
func (m *Response) XXX_DiscardUnknown() {
xxx_messageInfo_Response.DiscardUnknown(m)
}
var xxx_messageInfo_Response proto.InternalMessageInfo
func (m *Response) GetStatusCode() int32 {
if m != nil {
return m.StatusCode
}
return 0
}
func (m *Response) GetHeader() map[string]*Pair {
if m != nil {
return m.Header
}
return nil
}
func (m *Response) GetBody() string {
if m != nil {
return m.Body
}
return ""
}
// A HTTP event as RPC
// Forwarded by the event handler
type Event struct {
// e.g login
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// uuid
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
// unix timestamp of event
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
// event headers
Header map[string]*Pair `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// the event data
Data string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Event) Reset() { *m = Event{} }
func (m *Event) String() string { return proto.CompactTextString(m) }
func (*Event) ProtoMessage() {}
func (*Event) Descriptor() ([]byte, []int) {
return fileDescriptor_api_17a7876430d97ebd, []int{3}
}
func (m *Event) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Event.Unmarshal(m, b)
}
func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Event.Marshal(b, m, deterministic)
}
func (dst *Event) XXX_Merge(src proto.Message) {
xxx_messageInfo_Event.Merge(dst, src)
}
func (m *Event) XXX_Size() int {
return xxx_messageInfo_Event.Size(m)
}
func (m *Event) XXX_DiscardUnknown() {
xxx_messageInfo_Event.DiscardUnknown(m)
}
var xxx_messageInfo_Event proto.InternalMessageInfo
func (m *Event) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Event) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Event) GetTimestamp() int64 {
if m != nil {
return m.Timestamp
}
return 0
}
func (m *Event) GetHeader() map[string]*Pair {
if m != nil {
return m.Header
}
return nil
}
func (m *Event) GetData() string {
if m != nil {
return m.Data
}
return ""
}
func init() {
proto.RegisterType((*Pair)(nil), "go.api.Pair")
proto.RegisterType((*Request)(nil), "go.api.Request")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.GetEntry")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.HeaderEntry")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.PostEntry")
proto.RegisterType((*Response)(nil), "go.api.Response")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Response.HeaderEntry")
proto.RegisterType((*Event)(nil), "go.api.Event")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Event.HeaderEntry")
}
func init() {
proto.RegisterFile("github.com/micro/go-micro/api/proto/api.proto", fileDescriptor_api_17a7876430d97ebd)
}
var fileDescriptor_api_17a7876430d97ebd = []byte{
// 410 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xc1, 0x6e, 0xd4, 0x30,
0x10, 0x55, 0xe2, 0x24, 0x6d, 0x66, 0x11, 0x42, 0x3e, 0x20, 0x53, 0x2a, 0xb4, 0xca, 0x85, 0x15,
0x52, 0x13, 0x68, 0x39, 0x20, 0xae, 0xb0, 0x2a, 0xc7, 0xca, 0x7f, 0xe0, 0x6d, 0xac, 0xc4, 0x62,
0x13, 0x9b, 0xd8, 0xa9, 0xb4, 0x1f, 0xc7, 0x81, 0xcf, 0xe0, 0x6f, 0x90, 0x27, 0xde, 0xdd, 0xb2,
0x5a, 0x2e, 0x74, 0x6f, 0x2f, 0xf6, 0x9b, 0x37, 0x6f, 0xde, 0x38, 0xf0, 0xb6, 0x51, 0xae, 0x1d,
0x57, 0xe5, 0xbd, 0xee, 0xaa, 0x4e, 0xdd, 0x0f, 0xba, 0x6a, 0xf4, 0x95, 0x30, 0xaa, 0x32, 0x83,
0x76, 0xba, 0x12, 0x46, 0x95, 0x88, 0x68, 0xd6, 0xe8, 0x52, 0x18, 0x55, 0xbc, 0x87, 0xe4, 0x4e,
0xa8, 0x81, 0xbe, 0x00, 0xf2, 0x5d, 0x6e, 0x58, 0x34, 0x8f, 0x16, 0x39, 0xf7, 0x90, 0xbe, 0x84,
0xec, 0x41, 0xac, 0x47, 0x69, 0x59, 0x3c, 0x27, 0x8b, 0x9c, 0x87, 0xaf, 0xe2, 0x17, 0x81, 0x33,
0x2e, 0x7f, 0x8c, 0xd2, 0x3a, 0xcf, 0xe9, 0xa4, 0x6b, 0x75, 0x1d, 0x0a, 0xc3, 0x17, 0xa5, 0x90,
0x18, 0xe1, 0x5a, 0x16, 0xe3, 0x29, 0x62, 0x7a, 0x03, 0x59, 0x2b, 0x45, 0x2d, 0x07, 0x46, 0xe6,
0x64, 0x31, 0xbb, 0x7e, 0x5d, 0x4e, 0x16, 0xca, 0x20, 0x56, 0x7e, 0xc3, 0xdb, 0x65, 0xef, 0x86,
0x0d, 0x0f, 0x54, 0xfa, 0x0e, 0x48, 0x23, 0x1d, 0x4b, 0xb0, 0x82, 0x1d, 0x56, 0xdc, 0x4a, 0x37,
0xd1, 0x3d, 0x89, 0x5e, 0x41, 0x62, 0xb4, 0x75, 0x2c, 0x45, 0xf2, 0xab, 0x43, 0xf2, 0x9d, 0xb6,
0x81, 0x8d, 0x34, 0xef, 0x71, 0xa5, 0xeb, 0x0d, 0xcb, 0x26, 0x8f, 0x1e, 0xfb, 0x14, 0xc6, 0x61,
0xcd, 0xce, 0xa6, 0x14, 0xc6, 0x61, 0x7d, 0x71, 0x0b, 0xb3, 0x47, 0xbe, 0x8e, 0xc4, 0x54, 0x40,
0x8a, 0xc1, 0xe0, 0xac, 0xb3, 0xeb, 0x67, 0xdb, 0xb6, 0x3e, 0x55, 0x3e, 0x5d, 0x7d, 0x8e, 0x3f,
0x45, 0x17, 0x5f, 0xe1, 0x7c, 0x6b, 0xf7, 0x09, 0x2a, 0x4b, 0xc8, 0x77, 0x73, 0xfc, 0xbf, 0x4c,
0xf1, 0x33, 0x82, 0x73, 0x2e, 0xad, 0xd1, 0xbd, 0x95, 0xf4, 0x0d, 0x80, 0x75, 0xc2, 0x8d, 0xf6,
0x8b, 0xae, 0x25, 0xaa, 0xa5, 0xfc, 0xd1, 0x09, 0xfd, 0xb8, 0x5b, 0x5c, 0x8c, 0xc9, 0x5e, 0xee,
0x93, 0x9d, 0x14, 0x8e, 0x6e, 0x6e, 0x1b, 0x2f, 0xd9, 0xc7, 0x7b, 0xb2, 0x30, 0x8b, 0xdf, 0x11,
0xa4, 0xcb, 0x07, 0xd9, 0xe3, 0x16, 0x7b, 0xd1, 0xc9, 0x20, 0x82, 0x98, 0x3e, 0x87, 0x58, 0xd5,
0xe1, 0xed, 0xc5, 0xaa, 0xa6, 0x97, 0x90, 0x3b, 0xd5, 0x49, 0xeb, 0x44, 0x67, 0xd0, 0x0f, 0xe1,
0xfb, 0x03, 0xfa, 0x61, 0x37, 0x5e, 0xf2, 0xf7, 0xc3, 0xc1, 0x06, 0xff, 0x9a, 0xad, 0x16, 0x4e,
0xb0, 0x74, 0x6a, 0xea, 0xf1, 0xc9, 0x66, 0x5b, 0x65, 0xf8, 0x83, 0xde, 0xfc, 0x09, 0x00, 0x00,
0xff, 0xff, 0x7a, 0xb4, 0xd4, 0x8f, 0xcb, 0x03, 0x00, 0x00,
}
+43
View File
@@ -0,0 +1,43 @@
syntax = "proto3";
package go.api;
message Pair {
string key = 1;
repeated string values = 2;
}
// A HTTP request as RPC
// Forward by the api handler
message Request {
string method = 1;
string path = 2;
map<string, Pair> header = 3;
map<string, Pair> get = 4;
map<string, Pair> post = 5;
string body = 6; // raw request body; if not application/x-www-form-urlencoded
string url = 7;
}
// A HTTP response as RPC
// Expected response for the api handler
message Response {
int32 statusCode = 1;
map<string, Pair> header = 2;
string body = 3;
}
// A HTTP event as RPC
// Forwarded by the event handler
message Event {
// e.g login
string name = 1;
// uuid
string id = 2;
// unix timestamp of event
int64 timestamp = 3;
// event headers
map<string, Pair> header = 4;
// the event data
string data = 5;
}
+38
View File
@@ -0,0 +1,38 @@
// Package grpc resolves a grpc service like /greeter.Say/Hello to greeter service
package grpc
import (
"errors"
"net/http"
"strings"
"github.com/micro/go-micro/api/resolver"
)
type Resolver struct{}
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
// /foo.Bar/Service
if req.URL.Path == "/" {
return nil, errors.New("unknown name")
}
// [foo.Bar, Service]
parts := strings.Split(req.URL.Path[1:], "/")
// [foo, Bar]
name := strings.Split(parts[0], ".")
// foo
return &resolver.Endpoint{
Name: strings.Join(name[:len(name)-1], "."),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
func (r *Resolver) String() string {
return "grpc"
}
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{}
}
+27
View File
@@ -0,0 +1,27 @@
// Package host resolves using http host
package host
import (
"net/http"
"github.com/micro/go-micro/api/resolver"
)
type Resolver struct{}
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
return &resolver.Endpoint{
Name: req.Host,
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
func (r *Resolver) String() string {
return "host"
}
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{}
}
+45
View File
@@ -0,0 +1,45 @@
// Package micro provides a micro rpc resolver which prefixes a namespace
package micro
import (
"net/http"
"github.com/micro/go-micro/api/resolver"
)
// default resolver for legacy purposes
// it uses proxy routing to resolve names
// /foo becomes namespace.foo
// /v1/foo becomes namespace.v1.foo
type Resolver struct {
Options resolver.Options
}
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
var name, method string
switch r.Options.Handler {
// internal handlers
case "meta", "api", "rpc", "micro":
name, method = apiRoute(req.URL.Path)
default:
method = req.Method
name = proxyRoute(req.URL.Path)
}
return &resolver.Endpoint{
Name: name,
Method: method,
}, nil
}
func (r *Resolver) String() string {
return "micro"
}
// NewResolver creates a new micro resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{
Options: resolver.NewOptions(opts...),
}
}
+90
View File
@@ -0,0 +1,90 @@
package micro
import (
"path"
"regexp"
"strings"
)
var (
proxyRe = regexp.MustCompile("^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$")
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
)
// Translates /foo/bar/zool into api service go.micro.api.foo method Bar.Zool
// Translates /foo/bar into api service go.micro.api.foo method Foo.Bar
func apiRoute(p string) (string, string) {
p = path.Clean(p)
p = strings.TrimPrefix(p, "/")
parts := strings.Split(p, "/")
// If we've got two or less parts
// Use first part as service
// Use all parts as method
if len(parts) <= 2 {
name := parts[0]
return name, methodName(parts)
}
// Treat /v[0-9]+ as versioning where we have 3 parts
// /v1/foo/bar => service: v1.foo method: Foo.bar
if len(parts) == 3 && versionRe.Match([]byte(parts[0])) {
name := strings.Join(parts[:len(parts)-1], ".")
return name, methodName(parts[len(parts)-2:])
}
// Service is everything minus last two parts
// Method is the last two parts
name := strings.Join(parts[:len(parts)-2], ".")
return name, methodName(parts[len(parts)-2:])
}
func proxyRoute(p string) string {
parts := strings.Split(p, "/")
if len(parts) < 2 {
return ""
}
var service string
var alias string
// /[service]/methods
if len(parts) > 2 {
// /v1/[service]
if versionRe.MatchString(parts[1]) {
service = parts[1] + "." + parts[2]
alias = parts[2]
} else {
service = parts[1]
alias = parts[1]
}
// /[service]
} else {
service = parts[1]
alias = parts[1]
}
// check service name is valid
if !proxyRe.MatchString(alias) {
return ""
}
return service
}
func methodName(parts []string) string {
for i, part := range parts {
parts[i] = toCamel(part)
}
return strings.Join(parts, ".")
}
func toCamel(s string) string {
words := strings.Split(s, "-")
var out string
for _, word := range words {
out += strings.Title(word)
}
return out
}
+130
View File
@@ -0,0 +1,130 @@
package micro
import (
"testing"
)
func TestApiRoute(t *testing.T) {
testData := []struct {
path string
service string
method string
}{
{
"/foo/bar",
"foo",
"Foo.Bar",
},
{
"/foo/foo/bar",
"foo",
"Foo.Bar",
},
{
"/foo/bar/baz",
"foo",
"Bar.Baz",
},
{
"/foo/bar/baz-xyz",
"foo",
"Bar.BazXyz",
},
{
"/foo/bar/baz/cat",
"foo.bar",
"Baz.Cat",
},
{
"/foo/bar/baz/cat/car",
"foo.bar.baz",
"Cat.Car",
},
{
"/foo/fooBar/bazCat",
"foo",
"FooBar.BazCat",
},
{
"/v1/foo/bar",
"v1.foo",
"Foo.Bar",
},
{
"/v1/foo/bar/baz",
"v1.foo",
"Bar.Baz",
},
{
"/v1/foo/bar/baz/cat",
"v1.foo.bar",
"Baz.Cat",
},
}
for _, d := range testData {
s, m := apiRoute(d.path)
if d.service != s {
t.Fatalf("Expected service: %s for path: %s got: %s %s", d.service, d.path, s, m)
}
if d.method != m {
t.Fatalf("Expected service: %s for path: %s got: %s", d.method, d.path, m)
}
}
}
func TestProxyRoute(t *testing.T) {
testData := []struct {
path string
service string
}{
// no namespace
{
"/f",
"f",
},
{
"/f",
"f",
},
{
"/f-b",
"f-b",
},
{
"/foo/bar",
"foo",
},
{
"/foo-bar",
"foo-bar",
},
{
"/foo-bar-baz",
"foo-bar-baz",
},
{
"/foo/bar/bar",
"foo",
},
{
"/v1/foo/bar",
"v1.foo",
},
{
"/v1/foo/bar/baz",
"v1.foo",
},
{
"/v1/foo/bar/baz/cat",
"v1.foo",
},
}
for _, d := range testData {
s := proxyRoute(d.path)
if d.service != s {
t.Fatalf("Expected service: %s for path: %s got: %s", d.service, d.path, s)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
package resolver
// NewOptions returns new initialised options
func NewOptions(opts ...Option) Options {
var options Options
for _, o := range opts {
o(&options)
}
return options
}
// WithHandler sets the handler being used
func WithHandler(h string) Option {
return func(o *Options) {
o.Handler = h
}
}
// WithNamespace sets the namespace being used
func WithNamespace(n string) Option {
return func(o *Options) {
o.Namespace = n
}
}
+33
View File
@@ -0,0 +1,33 @@
// Package path resolves using http path
package path
import (
"errors"
"net/http"
"strings"
"github.com/micro/go-micro/api/resolver"
)
type Resolver struct{}
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
if req.URL.Path == "/" {
return nil, errors.New("unknown name")
}
parts := strings.Split(req.URL.Path[1:], "/")
return &resolver.Endpoint{
Name: parts[0],
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
func (r *Resolver) String() string {
return "path"
}
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{}
}
+31
View File
@@ -0,0 +1,31 @@
// Package resolver resolves a http request to an endpoint
package resolver
import (
"net/http"
)
// Resolver resolves requests to endpoints
type Resolver interface {
Resolve(r *http.Request) (*Endpoint, error)
String() string
}
// Endpoint is the endpoint for a http request
type Endpoint struct {
// e.g greeter
Name string
// HTTP Host e.g example.com
Host string
// HTTP Methods e.g GET, POST
Method string
// HTTP Path e.g /greeter.
Path string
}
type Options struct {
Handler string
Namespace string
}
type Option func(o *Options)
+59
View File
@@ -0,0 +1,59 @@
// Package vpath resolves using http path and recognised versioned urls
package vpath
import (
"errors"
"net/http"
"regexp"
"strings"
"github.com/micro/go-micro/api/resolver"
)
type Resolver struct{}
var (
re = regexp.MustCompile("^v[0-9]+$")
)
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
if req.URL.Path == "/" {
return nil, errors.New("unknown name")
}
parts := strings.Split(req.URL.Path[1:], "/")
if len(parts) == 1 {
return &resolver.Endpoint{
Name: parts[0],
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
// /v1/foo
if re.MatchString(parts[0]) {
return &resolver.Endpoint{
Name: parts[1],
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
return &resolver.Endpoint{
Name: parts[0],
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
func (r *Resolver) String() string {
return "path"
}
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{}
}
+61
View File
@@ -0,0 +1,61 @@
package router
import (
"github.com/micro/go-micro/api/resolver"
"github.com/micro/go-micro/api/resolver/micro"
"github.com/micro/go-micro/config/cmd"
"github.com/micro/go-micro/registry"
)
type Options struct {
Namespace string
Handler string
Registry registry.Registry
Resolver resolver.Resolver
}
type Option func(o *Options)
func NewOptions(opts ...Option) Options {
options := Options{
Handler: "meta",
Registry: *cmd.DefaultOptions().Registry,
}
for _, o := range opts {
o(&options)
}
if options.Resolver == nil {
options.Resolver = micro.NewResolver(
resolver.WithHandler(options.Handler),
resolver.WithNamespace(options.Namespace),
)
}
return options
}
func WithHandler(h string) Option {
return func(o *Options) {
o.Handler = h
}
}
func WithNamespace(ns string) Option {
return func(o *Options) {
o.Namespace = ns
}
}
func WithRegistry(r registry.Registry) Option {
return func(o *Options) {
o.Registry = r
}
}
func WithResolver(r resolver.Resolver) Option {
return func(o *Options) {
o.Resolver = r
}
}
+393
View File
@@ -0,0 +1,393 @@
// Package registry provides a dynamic api service router
package registry
import (
"errors"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/micro/go-micro/api"
"github.com/micro/go-micro/api/router"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/cache"
)
// router is the default router
type registryRouter struct {
exit chan bool
opts router.Options
// registry cache
rc cache.Cache
sync.RWMutex
eps map[string]*api.Service
}
func setNamespace(ns, name string) string {
ns = strings.TrimSpace(ns)
name = strings.TrimSpace(name)
// no namespace
if len(ns) == 0 {
return name
}
switch {
// has - suffix
case strings.HasSuffix(ns, "-"):
return strings.Replace(ns+name, ".", "-", -1)
// has . suffix
case strings.HasSuffix(ns, "."):
return ns + name
}
// default join .
return strings.Join([]string{ns, name}, ".")
}
func (r *registryRouter) isClosed() bool {
select {
case <-r.exit:
return true
default:
return false
}
}
// refresh list of api services
func (r *registryRouter) refresh() {
var attempts int
for {
services, err := r.opts.Registry.ListServices()
if err != nil {
attempts++
log.Println("Error listing endpoints", err)
time.Sleep(time.Duration(attempts) * time.Second)
continue
}
attempts = 0
// for each service, get service and store endpoints
for _, s := range services {
// only get services for this namespace
if !strings.HasPrefix(s.Name, r.opts.Namespace) {
continue
}
service, err := r.rc.GetService(s.Name)
if err != nil {
continue
}
r.store(service)
}
// refresh list in 10 minutes... cruft
select {
case <-time.After(time.Minute * 10):
case <-r.exit:
return
}
}
}
// process watch event
func (r *registryRouter) process(res *registry.Result) {
// skip these things
if res == nil || res.Service == nil || !strings.HasPrefix(res.Service.Name, r.opts.Namespace) {
return
}
// get entry from cache
service, err := r.rc.GetService(res.Service.Name)
if err != nil {
return
}
// update our local endpoints
r.store(service)
}
// store local endpoint cache
func (r *registryRouter) store(services []*registry.Service) {
// endpoints
eps := map[string]*api.Service{}
// services
names := map[string]bool{}
// create a new endpoint mapping
for _, service := range services {
// set names we need later
names[service.Name] = true
// map per endpoint
for _, endpoint := range service.Endpoints {
// create a key service:endpoint_name
key := fmt.Sprintf("%s:%s", service.Name, endpoint.Name)
// decode endpoint
end := api.Decode(endpoint.Metadata)
// if we got nothing skip
if err := api.Validate(end); err != nil {
continue
}
// try get endpoint
ep, ok := eps[key]
if !ok {
ep = &api.Service{Name: service.Name}
}
// overwrite the endpoint
ep.Endpoint = end
// append services
ep.Services = append(ep.Services, service)
// store it
eps[key] = ep
}
}
r.Lock()
defer r.Unlock()
// delete any existing eps for services we know
for key, service := range r.eps {
// skip what we don't care about
if !names[service.Name] {
continue
}
// ok we know this thing
// delete delete delete
delete(r.eps, key)
}
// now set the eps we have
for name, endpoint := range eps {
r.eps[name] = endpoint
}
}
// watch for endpoint changes
func (r *registryRouter) watch() {
var attempts int
for {
if r.isClosed() {
return
}
// watch for changes
w, err := r.opts.Registry.Watch()
if err != nil {
attempts++
log.Println("Error watching endpoints", err)
time.Sleep(time.Duration(attempts) * time.Second)
continue
}
ch := make(chan bool)
go func() {
select {
case <-ch:
w.Stop()
case <-r.exit:
w.Stop()
}
}()
// reset if we get here
attempts = 0
for {
// process next event
res, err := w.Next()
if err != nil {
log.Println("Error getting next endpoint", err)
close(ch)
break
}
r.process(res)
}
}
}
func (r *registryRouter) Options() router.Options {
return r.opts
}
func (r *registryRouter) Close() error {
select {
case <-r.exit:
return nil
default:
close(r.exit)
r.rc.Stop()
}
return nil
}
func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) {
if r.isClosed() {
return nil, errors.New("router closed")
}
r.RLock()
defer r.RUnlock()
// use the first match
// TODO: weighted matching
for _, e := range r.eps {
ep := e.Endpoint
// match
var pathMatch, hostMatch, methodMatch bool
// 1. try method GET, POST, PUT, etc
// 2. try host example.com, foobar.com, etc
// 3. try path /foo/bar, /bar/baz, etc
// 1. try match method
for _, m := range ep.Method {
if req.Method == m {
methodMatch = true
break
}
}
// no match on method pass
if len(ep.Method) > 0 && !methodMatch {
continue
}
// 2. try match host
for _, h := range ep.Host {
if req.Host == h {
hostMatch = true
break
}
}
// no match on host pass
if len(ep.Host) > 0 && !hostMatch {
continue
}
// 3. try match paths
for _, p := range ep.Path {
re, err := regexp.CompilePOSIX(p)
if err == nil && re.MatchString(req.URL.Path) {
pathMatch = true
break
}
}
// no match pass
if len(ep.Path) > 0 && !pathMatch {
continue
}
// TODO: Percentage traffic
// we got here, so its a match
return e, nil
}
// no match
return nil, errors.New("not found")
}
func (r *registryRouter) Route(req *http.Request) (*api.Service, error) {
if r.isClosed() {
return nil, errors.New("router closed")
}
// try get an endpoint
ep, err := r.Endpoint(req)
if err == nil {
return ep, nil
}
// error not nil
// ignore that shit
// TODO: don't ignore that shit
// get the service name
rp, err := r.opts.Resolver.Resolve(req)
if err != nil {
return nil, err
}
// service name
name := setNamespace(r.opts.Namespace, rp.Name)
// get service
services, err := r.rc.GetService(name)
if err != nil {
return nil, err
}
// only use endpoint matching when the meta handler is set aka api.Default
switch r.opts.Handler {
// rpc handlers
case "meta", "api", "rpc":
handler := r.opts.Handler
// set default handler to api
if r.opts.Handler == "meta" {
handler = "rpc"
}
// construct api service
return &api.Service{
Name: name,
Endpoint: &api.Endpoint{
Name: rp.Method,
Handler: handler,
},
Services: services,
}, nil
// http handler
case "http", "proxy", "web":
// construct api service
return &api.Service{
Name: name,
Endpoint: &api.Endpoint{
Name: req.URL.String(),
Handler: r.opts.Handler,
Host: []string{req.Host},
Method: []string{req.Method},
Path: []string{req.URL.Path},
},
Services: services,
}, nil
}
return nil, errors.New("unknown handler")
}
func newRouter(opts ...router.Option) *registryRouter {
options := router.NewOptions(opts...)
r := &registryRouter{
exit: make(chan bool),
opts: options,
rc: cache.New(options.Registry),
eps: make(map[string]*api.Service),
}
go r.watch()
go r.refresh()
return r
}
// NewRouter returns the default router
func NewRouter(opts ...router.Option) router.Router {
return newRouter(opts...)
}
+181
View File
@@ -0,0 +1,181 @@
package registry
import (
"fmt"
"net/http"
"net/url"
"testing"
"github.com/micro/go-micro/api"
)
func TestSetNamespace(t *testing.T) {
testCases := []struct {
namespace string
name string
expected string
}{
// default dotted path
{
"go.micro.api",
"foo",
"go.micro.api.foo",
},
// dotted end
{
"go.micro.api.",
"foo",
"go.micro.api.foo",
},
// dashed end
{
"go-micro-api-",
"foo",
"go-micro-api-foo",
},
// no namespace
{
"",
"foo",
"foo",
},
{
"go-micro-api-",
"v2.foo",
"go-micro-api-v2-foo",
},
}
for _, test := range testCases {
name := setNamespace(test.namespace, test.name)
if name != test.expected {
t.Fatalf("expected name %s got %s", test.expected, name)
}
}
}
func TestRouter(t *testing.T) {
r := newRouter()
compare := func(expect, got []string) bool {
// no data to compare, return true
if len(expect) == 0 && len(got) == 0 {
return true
}
// no data expected but got some return false
if len(expect) == 0 && len(got) > 0 {
return false
}
// compare expected with what we got
for _, e := range expect {
var seen bool
for _, g := range got {
if e == g {
seen = true
break
}
}
if !seen {
return false
}
}
// we're done, return true
return true
}
testData := []struct {
e *api.Endpoint
r *http.Request
m bool
}{
{
e: &api.Endpoint{
Name: "Foo.Bar",
Host: []string{"example.com"},
Method: []string{"GET"},
Path: []string{"/foo"},
},
r: &http.Request{
Host: "example.com",
Method: "GET",
URL: &url.URL{
Path: "/foo",
},
},
m: true,
},
{
e: &api.Endpoint{
Name: "Bar.Baz",
Host: []string{"example.com", "foo.com"},
Method: []string{"GET", "POST"},
Path: []string{"/foo/bar"},
},
r: &http.Request{
Host: "foo.com",
Method: "POST",
URL: &url.URL{
Path: "/foo/bar",
},
},
m: true,
},
{
e: &api.Endpoint{
Name: "Test.Cruft",
Host: []string{"example.com", "foo.com"},
Method: []string{"GET", "POST"},
Path: []string{"/xyz"},
},
r: &http.Request{
Host: "fail.com",
Method: "DELETE",
URL: &url.URL{
Path: "/test/fail",
},
},
m: false,
},
}
for _, d := range testData {
key := fmt.Sprintf("%s:%s", "test.service", d.e.Name)
r.eps[key] = &api.Service{
Endpoint: d.e,
}
}
for _, d := range testData {
e, err := r.Endpoint(d.r)
if d.m && err != nil {
t.Fatalf("expected match, got %v", err)
}
if !d.m && err == nil {
t.Fatal("expected error got match")
}
// skip testing the non match
if !d.m {
continue
}
ep := e.Endpoint
// test the match
if d.e.Name != ep.Name {
t.Fatalf("expected %v got %v", d.e.Name, ep.Name)
}
if ok := compare(d.e.Method, ep.Method); !ok {
t.Fatalf("expected %v got %v", d.e.Method, ep.Method)
}
if ok := compare(d.e.Path, ep.Path); !ok {
t.Fatalf("expected %v got %v", d.e.Path, ep.Path)
}
if ok := compare(d.e.Host, ep.Host); !ok {
t.Fatalf("expected %v got %v", d.e.Host, ep.Host)
}
}
}
+20
View File
@@ -0,0 +1,20 @@
// Package router provides api service routing
package router
import (
"net/http"
"github.com/micro/go-micro/api"
)
// Router is used to determine an endpoint for a request
type Router interface {
// Returns options
Options() Options
// Stop the router
Close() error
// Endpoint returns an api.Service endpoint or an error if it does not exist
Endpoint(r *http.Request) (*api.Service, error)
// Route returns an api.Service route
Route(r *http.Request) (*api.Service, error)
}
+98
View File
@@ -0,0 +1,98 @@
// Package http provides a http server with features; acme, cors, etc
package http
import (
"crypto/tls"
"net"
"net/http"
"os"
"sync"
"github.com/gorilla/handlers"
"github.com/micro/go-micro/api/server"
"github.com/micro/go-micro/util/log"
"golang.org/x/crypto/acme/autocert"
)
type httpServer struct {
mux *http.ServeMux
opts server.Options
mtx sync.RWMutex
address string
exit chan chan error
}
func NewServer(address string) server.Server {
return &httpServer{
opts: server.Options{},
mux: http.NewServeMux(),
address: address,
exit: make(chan chan error),
}
}
func (s *httpServer) Address() string {
s.mtx.RLock()
defer s.mtx.RUnlock()
return s.address
}
func (s *httpServer) Init(opts ...server.Option) error {
for _, o := range opts {
o(&s.opts)
}
return nil
}
func (s *httpServer) Handle(path string, handler http.Handler) {
s.mux.Handle(path, handlers.CombinedLoggingHandler(os.Stdout, handler))
}
func (s *httpServer) Start() error {
var l net.Listener
var err error
if s.opts.EnableACME {
// should we check the address to make sure its using :443?
l = autocert.NewListener(s.opts.ACMEHosts...)
} else if s.opts.EnableTLS && s.opts.TLSConfig != nil {
l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig)
} else {
// otherwise plain listen
l, err = net.Listen("tcp", s.address)
}
if err != nil {
return err
}
log.Logf("HTTP API Listening on %s", l.Addr().String())
s.mtx.Lock()
s.address = l.Addr().String()
s.mtx.Unlock()
go func() {
if err := http.Serve(l, s.mux); err != nil {
// temporary fix
//log.Fatal(err)
}
}()
go func() {
ch := <-s.exit
ch <- l.Close()
}()
return nil
}
func (s *httpServer) Stop() error {
ch := make(chan error)
s.exit <- ch
return <-ch
}
func (s *httpServer) String() string {
return "http"
}
+41
View File
@@ -0,0 +1,41 @@
package http
import (
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestHTTPServer(t *testing.T) {
testResponse := "hello world"
s := NewServer("localhost:0")
s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testResponse)
}))
if err := s.Start(); err != nil {
t.Fatal(err)
}
rsp, err := http.Get(fmt.Sprintf("http://%s/", s.Address()))
if err != nil {
t.Fatal(err)
}
defer rsp.Body.Close()
b, err := ioutil.ReadAll(rsp.Body)
if err != nil {
t.Fatal(err)
}
if string(b) != testResponse {
t.Fatalf("Unexpected response, got %s, expected %s", string(b), testResponse)
}
if err := s.Stop(); err != nil {
t.Fatal(err)
}
}
+38
View File
@@ -0,0 +1,38 @@
package server
import (
"crypto/tls"
)
type Option func(o *Options)
type Options struct {
EnableACME bool
EnableTLS bool
ACMEHosts []string
TLSConfig *tls.Config
}
func ACMEHosts(hosts ...string) Option {
return func(o *Options) {
o.ACMEHosts = hosts
}
}
func EnableACME(b bool) Option {
return func(o *Options) {
o.EnableACME = b
}
}
func EnableTLS(b bool) Option {
return func(o *Options) {
o.EnableTLS = b
}
}
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
+15
View File
@@ -0,0 +1,15 @@
// Package server provides an API gateway server which handles inbound requests
package server
import (
"net/http"
)
// Server serves api requests
type Server interface {
Address() string
Init(opts ...Option) error
Handle(path string, handler http.Handler)
Start() error
Stop() error
}
-137
View File
@@ -1,137 +0,0 @@
// Package auth provides authentication and authorization capability
package auth
import (
"context"
"errors"
"time"
)
const (
// BearerScheme used for Authorization header.
BearerScheme = "Bearer "
// ScopePublic is the scope applied to a rule to allow access to the public.
ScopePublic = ""
// ScopeAccount is the scope applied to a rule to limit to users with any valid account.
ScopeAccount = "*"
)
var (
// ErrInvalidToken is when the token provided is not valid.
ErrInvalidToken = errors.New("invalid token provided")
// ErrForbidden is when a user does not have the necessary scope to access a resource.
ErrForbidden = errors.New("resource forbidden")
)
// Auth provides authentication and authorization.
type Auth interface {
// Init the auth
Init(opts ...Option)
// Options set for auth
Options() Options
// Generate a new account
Generate(id string, opts ...GenerateOption) (*Account, error)
// Inspect a token
Inspect(token string) (*Account, error)
// Token generated using refresh token or credentials
Token(opts ...TokenOption) (*Token, error)
// String returns the name of the implementation
String() string
}
// Rules manages access to resources.
type Rules interface {
// Verify an account has access to a resource using the rules
Verify(acc *Account, res *Resource, opts ...VerifyOption) error
// Grant access to a resource
Grant(rule *Rule) error
// Revoke access to a resource
Revoke(rule *Rule) error
// List returns all the rules used to verify requests
List(...ListOption) ([]*Rule, error)
}
// Account provided by an auth provider.
type Account struct {
// Any other associated metadata
Metadata map[string]string `json:"metadata"`
// ID of the account e.g. email
ID string `json:"id"`
// Type of the account, e.g. service
Type string `json:"type"`
// Issuer of the account
Issuer string `json:"issuer"`
// Secret for the account, e.g. the password
Secret string `json:"secret"`
// Scopes the account has access to
Scopes []string `json:"scopes"`
}
// Token can be short or long lived.
type Token struct {
// Time of token creation
Created time.Time `json:"created"`
// Time of token expiry
Expiry time.Time `json:"expiry"`
// The token to be used for accessing resources
AccessToken string `json:"access_token"`
// RefreshToken to be used to generate a new token
RefreshToken string `json:"refresh_token"`
}
// Expired returns a boolean indicating if the token needs to be refreshed.
func (t *Token) Expired() bool {
return t.Expiry.Unix() < time.Now().Unix()
}
// Resource is an entity such as a user or.
type Resource struct {
// Name of the resource, e.g. go.micro.service.notes
Name string `json:"name"`
// Type of resource, e.g. service
Type string `json:"type"`
// Endpoint resource e.g NotesService.Create
Endpoint string `json:"endpoint"`
}
// Access defines the type of access a rule grants.
type Access int
const (
// AccessGranted to a resource.
AccessGranted Access = iota
// AccessDenied to a resource.
AccessDenied
)
// Rule is used to verify access to a resource.
type Rule struct {
// Resource the rule applies to
Resource *Resource
// ID of the rule, e.g. "public"
ID string
// Scope the rule requires, a blank scope indicates open to the public and * indicates the rule
// applies to any valid account
Scope string
// Access determines if the rule grants or denies access to the resource
Access Access
// Priority the rule should take when verifying a request, the higher the value the sooner the
// rule will be applied
Priority int32
}
type accountKey struct{}
// AccountFromContext gets the account from the context, which
// is set by the auth wrapper at the start of a call. If the account
// is not set, a nil account will be returned. The error is only returned
// when there was a problem retrieving an account.
func AccountFromContext(ctx context.Context) (*Account, bool) {
acc, ok := ctx.Value(accountKey{}).(*Account)
return acc, ok
}
// ContextWithAccount sets the account in the context.
func ContextWithAccount(ctx context.Context, account *Account) context.Context {
return context.WithValue(ctx, accountKey{}, account)
}
-157
View File
@@ -1,157 +0,0 @@
package jwt
import (
"sync"
"time"
jwtToken "github.com/micro/plugins/v5/auth/jwt/token"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/cmd"
)
func init() {
cmd.DefaultAuths["jwt"] = NewAuth
}
// NewAuth returns a new instance of the Auth service.
func NewAuth(opts ...auth.Option) auth.Auth {
j := new(jwt)
j.Init(opts...)
return j
}
func NewRules() auth.Rules {
return new(jwtRules)
}
type jwt struct {
sync.Mutex
options auth.Options
jwt jwtToken.Provider
}
type jwtRules struct {
sync.Mutex
rules []*auth.Rule
}
func (j *jwt) String() string {
return "jwt"
}
func (j *jwt) Init(opts ...auth.Option) {
j.Lock()
defer j.Unlock()
for _, o := range opts {
o(&j.options)
}
j.jwt = jwtToken.New(
jwtToken.WithPrivateKey(j.options.PrivateKey),
jwtToken.WithPublicKey(j.options.PublicKey),
)
}
func (j *jwt) Options() auth.Options {
j.Lock()
defer j.Unlock()
return j.options
}
func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
options := auth.NewGenerateOptions(opts...)
account := &auth.Account{
ID: id,
Type: options.Type,
Scopes: options.Scopes,
Metadata: options.Metadata,
Issuer: j.Options().Namespace,
}
// generate a JWT secret which can be provided to the Token() method
// and exchanged for an access token
secret, err := j.jwt.Generate(account)
if err != nil {
return nil, err
}
account.Secret = secret.Token
// return the account
return account, nil
}
func (j *jwtRules) Grant(rule *auth.Rule) error {
j.Lock()
defer j.Unlock()
j.rules = append(j.rules, rule)
return nil
}
func (j *jwtRules) Revoke(rule *auth.Rule) error {
j.Lock()
defer j.Unlock()
rules := make([]*auth.Rule, 0, len(j.rules))
for _, r := range j.rules {
if r.ID != rule.ID {
rules = append(rules, r)
}
}
j.rules = rules
return nil
}
func (j *jwtRules) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyOption) error {
j.Lock()
defer j.Unlock()
var options auth.VerifyOptions
for _, o := range opts {
o(&options)
}
return auth.Verify(j.rules, acc, res)
}
func (j *jwtRules) List(opts ...auth.ListOption) ([]*auth.Rule, error) {
j.Lock()
defer j.Unlock()
return j.rules, nil
}
func (j *jwt) Inspect(token string) (*auth.Account, error) {
return j.jwt.Inspect(token)
}
func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) {
options := auth.NewTokenOptions(opts...)
secret := options.RefreshToken
if len(options.Secret) > 0 {
secret = options.Secret
}
account, err := j.jwt.Inspect(secret)
if err != nil {
return nil, err
}
access, err := j.jwt.Generate(account, jwtToken.WithExpiry(options.Expiry))
if err != nil {
return nil, err
}
refresh, err := j.jwt.Generate(account, jwtToken.WithExpiry(options.Expiry+time.Hour))
if err != nil {
return nil, err
}
return &auth.Token{
Created: access.Created,
Expiry: access.Expiry,
AccessToken: access.Token,
RefreshToken: refresh.Token,
}, nil
}
-109
View File
@@ -1,109 +0,0 @@
package token
import (
"encoding/base64"
"time"
"github.com/dgrijalva/jwt-go"
"go-micro.dev/v5/auth"
)
// authClaims to be encoded in the JWT.
type authClaims struct {
Type string `json:"type"`
Scopes []string `json:"scopes"`
Metadata map[string]string `json:"metadata"`
jwt.StandardClaims
}
// JWT implementation of token provider.
type JWT struct {
opts Options
}
// New returns an initialized basic provider.
func New(opts ...Option) Provider {
return &JWT{
opts: NewOptions(opts...),
}
}
// Generate a new JWT.
func (j *JWT) Generate(acc *auth.Account, opts ...GenerateOption) (*Token, error) {
// decode the private key
priv, err := base64.StdEncoding.DecodeString(j.opts.PrivateKey)
if err != nil {
return nil, err
}
// parse the private key
key, err := jwt.ParseRSAPrivateKeyFromPEM(priv)
if err != nil {
return nil, ErrEncodingToken
}
// parse the options
options := NewGenerateOptions(opts...)
// generate the JWT
expiry := time.Now().Add(options.Expiry)
t := jwt.NewWithClaims(jwt.SigningMethodRS256, authClaims{
acc.Type, acc.Scopes, acc.Metadata, jwt.StandardClaims{
Subject: acc.ID,
Issuer: acc.Issuer,
ExpiresAt: expiry.Unix(),
},
})
tok, err := t.SignedString(key)
if err != nil {
return nil, err
}
// return the token
return &Token{
Token: tok,
Expiry: expiry,
Created: time.Now(),
}, nil
}
// Inspect a JWT.
func (j *JWT) Inspect(t string) (*auth.Account, error) {
// decode the public key
pub, err := base64.StdEncoding.DecodeString(j.opts.PublicKey)
if err != nil {
return nil, err
}
// parse the public key
res, err := jwt.ParseWithClaims(t, &authClaims{}, func(token *jwt.Token) (interface{}, error) {
return jwt.ParseRSAPublicKeyFromPEM(pub)
})
if err != nil {
return nil, ErrInvalidToken
}
// validate the token
if !res.Valid {
return nil, ErrInvalidToken
}
claims, ok := res.Claims.(*authClaims)
if !ok {
return nil, ErrInvalidToken
}
// return the token
return &auth.Account{
ID: claims.Subject,
Issuer: claims.Issuer,
Type: claims.Type,
Scopes: claims.Scopes,
Metadata: claims.Metadata,
}, nil
}
// String returns JWT.
func (j *JWT) String() string {
return "jwt"
}
-85
View File
@@ -1,85 +0,0 @@
package token
import (
"os"
"testing"
"time"
"go-micro.dev/v5/auth"
)
func TestGenerate(t *testing.T) {
privKey, err := os.ReadFile("test/sample_key")
if err != nil {
t.Fatalf("Unable to read private key: %v", err)
}
j := New(
WithPrivateKey(string(privKey)),
)
_, err = j.Generate(&auth.Account{ID: "test"})
if err != nil {
t.Fatalf("Generate returned %v error, expected nil", err)
}
}
func TestInspect(t *testing.T) {
pubKey, err := os.ReadFile("test/sample_key.pub")
if err != nil {
t.Fatalf("Unable to read public key: %v", err)
}
privKey, err := os.ReadFile("test/sample_key")
if err != nil {
t.Fatalf("Unable to read private key: %v", err)
}
j := New(
WithPublicKey(string(pubKey)),
WithPrivateKey(string(privKey)),
)
t.Run("Valid token", func(t *testing.T) {
md := map[string]string{"foo": "bar"}
scopes := []string{"admin"}
subject := "test"
acc := &auth.Account{ID: subject, Scopes: scopes, Metadata: md}
tok, err := j.Generate(acc)
if err != nil {
t.Fatalf("Generate returned %v error, expected nil", err)
}
tok2, err := j.Inspect(tok.Token)
if err != nil {
t.Fatalf("Inspect returned %v error, expected nil", err)
}
if acc.ID != subject {
t.Errorf("Inspect returned %v as the token subject, expected %v", acc.ID, subject)
}
if len(tok2.Scopes) != len(scopes) {
t.Errorf("Inspect returned %v scopes, expected %v", len(tok2.Scopes), len(scopes))
}
if len(tok2.Metadata) != len(md) {
t.Errorf("Inspect returned %v as the token metadata, expected %v", tok2.Metadata, md)
}
})
t.Run("Expired token", func(t *testing.T) {
tok, err := j.Generate(&auth.Account{}, WithExpiry(-10*time.Second))
if err != nil {
t.Fatalf("Generate returned %v error, expected nil", err)
}
if _, err = j.Inspect(tok.Token); err != ErrInvalidToken {
t.Fatalf("Inspect returned %v error, expected %v", err, ErrInvalidToken)
}
})
t.Run("Invalid token", func(t *testing.T) {
_, err := j.Inspect("Invalid token")
if err != ErrInvalidToken {
t.Fatalf("Inspect returned %v error, expected %v", err, ErrInvalidToken)
}
})
}
-78
View File
@@ -1,78 +0,0 @@
package token
import (
"time"
"go-micro.dev/v5/store"
)
type Options struct {
// Store to persist the tokens
Store store.Store
// PublicKey base64 encoded, used by JWT
PublicKey string
// PrivateKey base64 encoded, used by JWT
PrivateKey string
}
type Option func(o *Options)
// WithStore sets the token providers store.
func WithStore(s store.Store) Option {
return func(o *Options) {
o.Store = s
}
}
// WithPublicKey sets the JWT public key.
func WithPublicKey(key string) Option {
return func(o *Options) {
o.PublicKey = key
}
}
// WithPrivateKey sets the JWT private key.
func WithPrivateKey(key string) Option {
return func(o *Options) {
o.PrivateKey = key
}
}
func NewOptions(opts ...Option) Options {
var options Options
for _, o := range opts {
o(&options)
}
// set default store
if options.Store == nil {
options.Store = store.DefaultStore
}
return options
}
type GenerateOptions struct {
// Expiry for the token
Expiry time.Duration
}
type GenerateOption func(o *GenerateOptions)
// WithExpiry for the generated account's token expires.
func WithExpiry(d time.Duration) GenerateOption {
return func(o *GenerateOptions) {
o.Expiry = d
}
}
// NewGenerateOptions from a slice of options.
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
var options GenerateOptions
for _, o := range opts {
o(&options)
}
// set default Expiry of token
if options.Expiry == 0 {
options.Expiry = time.Minute * 15
}
return options
}
-1
View File
@@ -1 +0,0 @@
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS3dJQkFBS0NBZ0VBOFNiSlA1WGJFaWRSbTViMnNOcExHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkCi9SbDkvMXBNVjdNaU8zTEh3dGhIQzJCUllxcisxd0Zkb1pDR0JZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUKMEJIL2xYUU1xeUVxRjVNSTJ6ZWpDNHpNenIxNU9OK2dFNEpuaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtLwptVWRJVC9MYUY3a1F4eVlLNVZLbitOZ09Xek1sektBQXBDbjdUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsCm85akRqbFk1b0JPY3pmcWVOV0hLNUdYQjdRd3BMTmg5NDZQelpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDUKd2xFcThoTmhtaG01Tk5lL08rR2dqQkROU2ZVaDA2K3E0bmdtYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1bwpSdFFoZ2lZOTEwcFBmOWJhdVhXcXdVQ1VhNHFzSHpqS1IwTC9OMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVCnJnTHJQYkVCOWVnY0drMzgrYnBLczNaNlJyNSt0bkQxQklQSUZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVUKVEdEeFV4OG9qOFZJZVJuV0RxNk1jMWlKcDhVeWNpQklUUnR3NGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMApsYVF6QXVQM2FpV1hJTXAyc2M4U2MrQmwrTGpYbUJveEJyYUJIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YCmdGS1NzSW5IRHJIVk95V1BCZTNmYWRFYzc3YituYi9leE96cjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUEKQVFLQ0FnRUFqUzc1Q2VvUlRRcUtBNzZaaFNiNGEzNVlKRENtcEpSazFsRTNKYnFzNFYxRnhXaDBjZmJYeG9VMgpSdTRRYjUrZWhsdWJGSFQ2a1BxdG9uRWhRVExjMUNmVE9WbHJOb3hocDVZM2ZyUmlQcnNnNXcwK1R3RUtrcFJUCnltanJQTXdQbGxCM2U0NmVaYmVXWGc3R3FFVmptMGcxVFRRK0tocVM4R0w3VGJlTFhRN1ZTem9ydTNCNVRKMVEKeEN6TVB0dnQ2eDYrU3JrcmhvZG1iT3VNRkpDam1TbWxmck9pZzQ4Zkc3NUpERHRObXpLWHBEUVJpYUNodFJhVQpQRHpmUTlTamhYdFFqdkZvWFFFT3BqdkZVRjR2WldNUWNQNUw1VklDM3JRSWp4MFNzQTN6S0FwakVUbjJHNjN2CktZby8zVWttbzhkUCtGRHA3NCs5a3pLNHFFaFJycEl3bEtiN0VOZWtDUXZqUFl1K3pyKzMyUXdQNTJ2L2FveWQKdjJJaUY3M2laTU1vZDhhYjJuQStyVEI2T0cvOVlSYk5kV21tay9VTi9jUHYrN214TmZ6Y1d1ZU1XcThxMXh4eAptNTNpR0NSQ29PQ1lDQk4zcUFkb1JwYW5xd3lCOUxrLzFCQjBHUld3MjgxK3VhNXNYRnZBVDBKeTVURnduMncvClU1MlJKWFlNOXVhMFBvd214b0RDUWRuNFZYVkdNZGdXaHN4aXhHRlYwOUZObWJJQWJaN0xaWGtkS1gzc1ZVbTcKWU1WYWIzVVo2bEhtdXYzT1NzcHNVUlRqN1hiRzZpaVVlaDU1aW91OENWbnRndWtFcnEzQTQwT05FVzhjNDBzOQphVTBGaSs4eWZpQTViaVZHLzF0bWlucUVERkhuQStnWk1xNEhlSkZxcWZxaEZKa1JwRGtDZ2dFQkFQeGR1NGNKCm5Da1duZDdPWFlHMVM3UDdkVWhRUzgwSDlteW9uZFc5bGFCQm84RWRPeTVTZzNOUmsxQ2pNZFZ1a3FMcjhJSnkKeStLWk15SVpvSlJvbllaMEtIUUVMR3ZLbzFOS2NLQ1FJbnYvWHVCdFJpRzBVb1pQNVkwN0RpRFBRQWpYUjlXUwpBc0EzMmQ1eEtFOC91Y3h0MjVQVzJFakNBUmtVeHQ5d0tKazN3bC9JdXVYRlExTDdDWjJsOVlFUjlHeWxUbzhNCmxXUEY3YndtUFV4UVNKaTNVS0FjTzZweTVUU1lkdWQ2aGpQeXJwSXByNU42VGpmTlRFWkVBeU9LbXVpOHVkUkoKMUg3T3RQVEhGZElKQjNrNEJnRDZtRE1HbjB2SXBLaDhZN3NtRUZBbFkvaXlCZjMvOHk5VHVMb1BycEdqR3RHbgp4Y2RpMHFud2p0SGFNbFVDZ2dFQkFQU2Z0dVFCQ2dTU2JLUSswUEFSR2VVeEQyTmlvZk1teENNTmdHUzJ5Ull3CjRGaGV4ZWkwMVJoaFk1NjE3UjduR1dzb0czd1RQa3dvRTJtbE1aQkoxeWEvUU9RRnQ3WG02OVl0RGh0T2FWbDgKL0o4dlVuSTBtWmxtT2pjTlRoYnVPZDlNSDlRdGxIRUMxMlhYdHJNb3Fsb0U2a05TT0pJalNxYm9wcDRXc1BqcApvZTZ0Nkdyd1RhOHBHeUJWWS90Mi85Ym5ORHVPVlpjODBaODdtY2gzcDNQclBqU3h5di9saGxYMFMwYUdHTkhTCk1XVjdUa25OaGo1TWlIRXFnZ1pZemtBWTkyd1JoVENnU1A2M0VNcitUWXFudXVuMXJHbndPYm95TDR2aFRpV0UKcU42UDNCTFlCZ1FpMllDTDludEJrOEl6RHZyd096dW5GVnhhZ0g5SVVoY0NnZ0VCQUwzQXlLa1BlOENWUmR6cQpzL284VkJDZmFSOFhhUGRnSGxTek1BSXZpNXEwNENqckRyMlV3MHZwTVdnM1hOZ0xUT3g5bFJpd3NrYk9SRmxHCmhhd3hRUWlBdkk0SE9WTlBTU0R1WHVNTG5USTQ0S0RFNlMrY2cxU0VMS2pWbDVqcDNFOEpkL1RJMVpLc0xBQUsKZTNHakM5UC9ZbE8xL21ndW4xNjVkWk01cFAwWHBPb2FaeFV2RHFFTktyekR0V1g0RngyOTZlUzdaSFJodFpCNwovQ2t1VUhlcmxrN2RDNnZzdWhTaTh2eTM3c0tPbmQ0K3c4cVM4czhZYVZxSDl3ZzVScUxxakp0bmJBUnc3alVDCm9KQ053M1hNdnc3clhaYzRTbnhVQUNMRGJNV2lLQy9xL1ZGWW9oTEs2WkpUVkJscWd5cjBSYzBRWmpDMlNJb0kKMjRwRWt3VUNnZ0VCQUpqb0FJVVNsVFY0WlVwaExXN3g4WkxPa01UWjBVdFFyd2NPR0hSYndPUUxGeUNGMVFWNQppejNiR2s4SmZyZHpVdk1sTmREZm9uQXVHTHhQa3VTVEUxWlg4L0xVRkJveXhyV3dvZ0cxaUtwME11QTV6em90CjROai9DbUtCQVkvWnh2anA5M2RFS21aZGxWQkdmeUFMeWpmTW5MWUovZXh5L09YSnhPUktZTUttSHg4M08zRWsKMWhvb0FwbTZabTIzMjRGME1iVU1ham5Idld2ZjhHZGJTNk5zcHd4L0dkbk1tYVMrdUJMVUhVMkNLbmc1bEIwVAp4OWJITmY0dXlPbTR0dXRmNzhCd1R5V3UreEdrVW0zZ2VZMnkvR1hqdDZyY2l1ajFGNzFDenZzcXFmZThTcDdJCnd6SHdxcTNzVHR5S2lCYTZuYUdEYWpNR1pKYSt4MVZJV204Q2dnRUJBT001ajFZR25Ba0pxR0czQWJSVDIvNUMKaVVxN0loYkswOGZsSGs5a2YwUlVjZWc0ZVlKY3dIRXJVaE4rdWQyLzE3MC81dDYra0JUdTVZOUg3bkpLREtESQpoeEg5SStyamNlVkR0RVNTRkluSXdDQ1lrOHhOUzZ0cHZMV1U5b0pibGFKMlZsalV2NGRFWGVQb0hkREh1Zk9ZClVLa0lsV2E3Uit1QzNEOHF5U1JrQnFLa3ZXZ1RxcFNmTVNkc1ZTeFIzU2Q4SVhFSHFjTDNUNEtMWGtYNEdEamYKMmZOSTFpZkx6ekhJMTN3Tk5IUTVRNU9SUC9pell2QzVzZkx4U2ZIUXJiMXJZVkpKWkI5ZjVBUjRmWFpHSVFsbApjMG8xd0JmZFlqMnZxVDlpR09IQnNSSTlSL2M2RzJQcUt3aFRpSzJVR2lmVFNEUVFuUkF6b2tpQVkrbE8vUjQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
-1
View File
@@ -1 +0,0 @@
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS3dJQkFBS0NBZ0VBOFNiSlA1WGJFaWRSbTViMnNOcExHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkCi9SbDkvMXBNVjdNaU8zTEh3dGhIQzJCUllxcisxd0Zkb1pDR0JZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUKMEJIL2xYUU1xeUVxRjVNSTJ6ZWpDNHpNenIxNU9OK2dFNEpuaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtLwptVWRJVC9MYUY3a1F4eVlLNVZLbitOZ09Xek1sektBQXBDbjdUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsCm85akRqbFk1b0JPY3pmcWVOV0hLNUdYQjdRd3BMTmg5NDZQelpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDUKd2xFcThoTmhtaG01Tk5lL08rR2dqQkROU2ZVaDA2K3E0bmdtYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1bwpSdFFoZ2lZOTEwcFBmOWJhdVhXcXdVQ1VhNHFzSHpqS1IwTC9OMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVCnJnTHJQYkVCOWVnY0drMzgrYnBLczNaNlJyNSt0bkQxQklQSUZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVUKVEdEeFV4OG9qOFZJZVJuV0RxNk1jMWlKcDhVeWNpQklUUnR3NGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMApsYVF6QXVQM2FpV1hJTXAyc2M4U2MrQmwrTGpYbUJveEJyYUJIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YCmdGS1NzSW5IRHJIVk95V1BCZTNmYWRFYzc3YituYi9leE96cjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUEKQVFLQ0FnRUFqUzc1Q2VvUlRRcUtBNzZaaFNiNGEzNVlKRENtcEpSazFsRTNKYnFzNFYxRnhXaDBjZmJYeG9VMgpSdTRRYjUrZWhsdWJGSFQ2a1BxdG9uRWhRVExjMUNmVE9WbHJOb3hocDVZM2ZyUmlQcnNnNXcwK1R3RUtrcFJUCnltanJQTXdQbGxCM2U0NmVaYmVXWGc3R3FFVmptMGcxVFRRK0tocVM4R0w3VGJlTFhRN1ZTem9ydTNCNVRKMVEKeEN6TVB0dnQ2eDYrU3JrcmhvZG1iT3VNRkpDam1TbWxmck9pZzQ4Zkc3NUpERHRObXpLWHBEUVJpYUNodFJhVQpQRHpmUTlTamhYdFFqdkZvWFFFT3BqdkZVRjR2WldNUWNQNUw1VklDM3JRSWp4MFNzQTN6S0FwakVUbjJHNjN2CktZby8zVWttbzhkUCtGRHA3NCs5a3pLNHFFaFJycEl3bEtiN0VOZWtDUXZqUFl1K3pyKzMyUXdQNTJ2L2FveWQKdjJJaUY3M2laTU1vZDhhYjJuQStyVEI2T0cvOVlSYk5kV21tay9VTi9jUHYrN214TmZ6Y1d1ZU1XcThxMXh4eAptNTNpR0NSQ29PQ1lDQk4zcUFkb1JwYW5xd3lCOUxrLzFCQjBHUld3MjgxK3VhNXNYRnZBVDBKeTVURnduMncvClU1MlJKWFlNOXVhMFBvd214b0RDUWRuNFZYVkdNZGdXaHN4aXhHRlYwOUZObWJJQWJaN0xaWGtkS1gzc1ZVbTcKWU1WYWIzVVo2bEhtdXYzT1NzcHNVUlRqN1hiRzZpaVVlaDU1aW91OENWbnRndWtFcnEzQTQwT05FVzhjNDBzOQphVTBGaSs4eWZpQTViaVZHLzF0bWlucUVERkhuQStnWk1xNEhlSkZxcWZxaEZKa1JwRGtDZ2dFQkFQeGR1NGNKCm5Da1duZDdPWFlHMVM3UDdkVWhRUzgwSDlteW9uZFc5bGFCQm84RWRPeTVTZzNOUmsxQ2pNZFZ1a3FMcjhJSnkKeStLWk15SVpvSlJvbllaMEtIUUVMR3ZLbzFOS2NLQ1FJbnYvWHVCdFJpRzBVb1pQNVkwN0RpRFBRQWpYUjlXUwpBc0EzMmQ1eEtFOC91Y3h0MjVQVzJFakNBUmtVeHQ5d0tKazN3bC9JdXVYRlExTDdDWjJsOVlFUjlHeWxUbzhNCmxXUEY3YndtUFV4UVNKaTNVS0FjTzZweTVUU1lkdWQ2aGpQeXJwSXByNU42VGpmTlRFWkVBeU9LbXVpOHVkUkoKMUg3T3RQVEhGZElKQjNrNEJnRDZtRE1HbjB2SXBLaDhZN3NtRUZBbFkvaXlCZjMvOHk5VHVMb1BycEdqR3RHbgp4Y2RpMHFud2p0SGFNbFVDZ2dFQkFQU2Z0dVFCQ2dTU2JLUSswUEFSR2VVeEQyTmlvZk1teENNTmdHUzJ5Ull3CjRGaGV4ZWkwMVJoaFk1NjE3UjduR1dzb0czd1RQa3dvRTJtbE1aQkoxeWEvUU9RRnQ3WG02OVl0RGh0T2FWbDgKL0o4dlVuSTBtWmxtT2pjTlRoYnVPZDlNSDlRdGxIRUMxMlhYdHJNb3Fsb0U2a05TT0pJalNxYm9wcDRXc1BqcApvZTZ0Nkdyd1RhOHBHeUJWWS90Mi85Ym5ORHVPVlpjODBaODdtY2gzcDNQclBqU3h5di9saGxYMFMwYUdHTkhTCk1XVjdUa25OaGo1TWlIRXFnZ1pZemtBWTkyd1JoVENnU1A2M0VNcitUWXFudXVuMXJHbndPYm95TDR2aFRpV0UKcU42UDNCTFlCZ1FpMllDTDludEJrOEl6RHZyd096dW5GVnhhZ0g5SVVoY0NnZ0VCQUwzQXlLa1BlOENWUmR6cQpzL284VkJDZmFSOFhhUGRnSGxTek1BSXZpNXEwNENqckRyMlV3MHZwTVdnM1hOZ0xUT3g5bFJpd3NrYk9SRmxHCmhhd3hRUWlBdkk0SE9WTlBTU0R1WHVNTG5USTQ0S0RFNlMrY2cxU0VMS2pWbDVqcDNFOEpkL1RJMVpLc0xBQUsKZTNHakM5UC9ZbE8xL21ndW4xNjVkWk01cFAwWHBPb2FaeFV2RHFFTktyekR0V1g0RngyOTZlUzdaSFJodFpCNwovQ2t1VUhlcmxrN2RDNnZzdWhTaTh2eTM3c0tPbmQ0K3c4cVM4czhZYVZxSDl3ZzVScUxxakp0bmJBUnc3alVDCm9KQ053M1hNdnc3clhaYzRTbnhVQUNMRGJNV2lLQy9xL1ZGWW9oTEs2WkpUVkJscWd5cjBSYzBRWmpDMlNJb0kKMjRwRWt3VUNnZ0VCQUpqb0FJVVNsVFY0WlVwaExXN3g4WkxPa01UWjBVdFFyd2NPR0hSYndPUUxGeUNGMVFWNQppejNiR2s4SmZyZHpVdk1sTmREZm9uQXVHTHhQa3VTVEUxWlg4L0xVRkJveXhyV3dvZ0cxaUtwME11QTV6em90CjROai9DbUtCQVkvWnh2anA5M2RFS21aZGxWQkdmeUFMeWpmTW5MWUovZXh5L09YSnhPUktZTUttSHg4M08zRWsKMWhvb0FwbTZabTIzMjRGME1iVU1ham5Idld2ZjhHZGJTNk5zcHd4L0dkbk1tYVMrdUJMVUhVMkNLbmc1bEIwVAp4OWJITmY0dXlPbTR0dXRmNzhCd1R5V3UreEdrVW0zZ2VZMnkvR1hqdDZyY2l1ajFGNzFDenZzcXFmZThTcDdJCnd6SHdxcTNzVHR5S2lCYTZuYUdEYWpNR1pKYSt4MVZJV204Q2dnRUJBT001ajFZR25Ba0pxR0czQWJSVDIvNUMKaVVxN0loYkswOGZsSGs5a2YwUlVjZWc0ZVlKY3dIRXJVaE4rdWQyLzE3MC81dDYra0JUdTVZOUg3bkpLREtESQpoeEg5SStyamNlVkR0RVNTRkluSXdDQ1lrOHhOUzZ0cHZMV1U5b0pibGFKMlZsalV2NGRFWGVQb0hkREh1Zk9ZClVLa0lsV2E3Uit1QzNEOHF5U1JrQnFLa3ZXZ1RxcFNmTVNkc1ZTeFIzU2Q4SVhFSHFjTDNUNEtMWGtYNEdEamYKMmZOSTFpZkx6ekhJMTN3Tk5IUTVRNU9SUC9pell2QzVzZkx4U2ZIUXJiMXJZVkpKWkI5ZjVBUjRmWFpHSVFsbApjMG8xd0JmZFlqMnZxVDlpR09IQnNSSTlSL2M2RzJQcUt3aFRpSzJVR2lmVFNEUVFuUkF6b2tpQVkrbE8vUjQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
-1
View File
@@ -1 +0,0 @@
LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUE4U2JKUDVYYkVpZFJtNWIyc05wTApHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkL1JsOS8xcE1WN01pTzNMSHd0aEhDMkJSWXFyKzF3RmRvWkNHCkJZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUwQkgvbFhRTXF5RXFGNU1JMnplakM0ek16cjE1T04rZ0U0Sm4KaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtL21VZElUL0xhRjdrUXh5WUs1VktuK05nT1d6TWx6S0FBcENuNwpUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsbzlqRGpsWTVvQk9jemZxZU5XSEs1R1hCN1F3cExOaDk0NlB6ClpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDV3bEVxOGhOaG1obTVOTmUvTytHZ2pCRE5TZlVoMDYrcTRuZ20KYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1b1J0UWhnaVk5MTBwUGY5YmF1WFdxd1VDVWE0cXNIempLUjBMLwpOMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVcmdMclBiRUI5ZWdjR2szOCticEtzM1o2UnI1K3RuRDFCSVBJCkZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVVUR0R4VXg4b2o4VkllUm5XRHE2TWMxaUpwOFV5Y2lCSVRSdHcKNGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMGxhUXpBdVAzYWlXWElNcDJzYzhTYytCbCtMalhtQm94QnJhQgpIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YZ0ZLU3NJbkhEckhWT3lXUEJlM2ZhZEVjNzdiK25iL2V4T3pyCjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=
-33
View File
@@ -1,33 +0,0 @@
package token
import (
"errors"
"time"
"go-micro.dev/v5/auth"
)
var (
// ErrNotFound is returned when a token cannot be found.
ErrNotFound = errors.New("token not found")
// ErrEncodingToken is returned when the service encounters an error during encoding.
ErrEncodingToken = errors.New("error encoding the token")
// ErrInvalidToken is returned when the token provided is not valid.
ErrInvalidToken = errors.New("invalid token provided")
)
// Provider generates and inspects tokens.
type Provider interface {
Generate(account *auth.Account, opts ...GenerateOption) (*Token, error)
Inspect(token string) (*auth.Account, error)
String() string
}
type Token struct {
// The actual token
Token string `json:"token"`
// Time of token creation
Created time.Time `json:"created"`
// Time of token expiry
Expiry time.Time `json:"expiry"`
}
-91
View File
@@ -1,91 +0,0 @@
package auth
import (
"github.com/google/uuid"
)
var (
DefaultAuth = NewAuth()
)
func NewAuth(opts ...Option) Auth {
options := Options{}
for _, o := range opts {
o(&options)
}
return &noop{
opts: options,
}
}
func NewRules() Rules {
return new(noopRules)
}
type noop struct {
opts Options
}
type noopRules struct{}
// String returns the name of the implementation.
func (n *noop) String() string {
return "noop"
}
// Init the auth.
func (n *noop) Init(opts ...Option) {
for _, o := range opts {
o(&n.opts)
}
}
// Options set for auth.
func (n *noop) Options() Options {
return n.opts
}
// Generate a new account.
func (n *noop) Generate(id string, opts ...GenerateOption) (*Account, error) {
options := NewGenerateOptions(opts...)
return &Account{
ID: id,
Secret: options.Secret,
Metadata: options.Metadata,
Scopes: options.Scopes,
Issuer: n.Options().Namespace,
}, nil
}
// Grant access to a resource.
func (n *noopRules) Grant(rule *Rule) error {
return nil
}
// Revoke access to a resource.
func (n *noopRules) Revoke(rule *Rule) error {
return nil
}
// Rules used to verify requests
// Verify an account has access to a resource.
func (n *noopRules) Verify(acc *Account, res *Resource, opts ...VerifyOption) error {
return nil
}
func (n *noopRules) List(opts ...ListOption) ([]*Rule, error) {
return []*Rule{}, nil
}
// Inspect a token.
func (n *noop) Inspect(token string) (*Account, error) {
return &Account{ID: uuid.New().String(), Issuer: n.Options().Namespace}, nil
}
// Token generation using an account id and secret.
func (n *noop) Token(opts ...TokenOption) (*Token, error) {
return &Token{}, nil
}
-222
View File
@@ -1,222 +0,0 @@
package auth
import (
"context"
"time"
"go-micro.dev/v5/logger"
)
func NewOptions(opts ...Option) Options {
options := Options{
Logger: logger.DefaultLogger,
}
for _, o := range opts {
o(&options)
}
return options
}
type Options struct {
// Logger is the underline logger
Logger logger.Logger
// Token is the services token used to authenticate itself
Token *Token
// Namespace the service belongs to
Namespace string
// ID is the services auth ID
ID string
// Secret is used to authenticate the service
Secret string
// PublicKey for decoding JWTs
PublicKey string
// PrivateKey for encoding JWTs
PrivateKey string
// Addrs sets the addresses of auth
Addrs []string
}
type Option func(o *Options)
// Addrs is the auth addresses to use.
func Addrs(addrs ...string) Option {
return func(o *Options) {
o.Addrs = addrs
}
}
// Namespace the service belongs to.
func Namespace(n string) Option {
return func(o *Options) {
o.Namespace = n
}
}
// PublicKey is the JWT public key.
func PublicKey(key string) Option {
return func(o *Options) {
o.PublicKey = key
}
}
// PrivateKey is the JWT private key.
func PrivateKey(key string) Option {
return func(o *Options) {
o.PrivateKey = key
}
}
// WithLogger sets the underline logger.
func WithLogger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Credentials sets the auth credentials.
func Credentials(id, secret string) Option {
return func(o *Options) {
o.ID = id
o.Secret = secret
}
}
// ClientToken sets the auth token to use when making requests.
func ClientToken(token *Token) Option {
return func(o *Options) {
o.Token = token
}
}
type GenerateOptions struct {
// Metadata associated with the account
Metadata map[string]string
// Provider of the account, e.g. oauth
Provider string
// Type of the account, e.g. user
Type string
// Secret used to authenticate the account
Secret string
// Scopes the account has access too
Scopes []string
}
type GenerateOption func(o *GenerateOptions)
// WithSecret for the generated account.
func WithSecret(s string) GenerateOption {
return func(o *GenerateOptions) {
o.Secret = s
}
}
// WithType for the generated account.
func WithType(t string) GenerateOption {
return func(o *GenerateOptions) {
o.Type = t
}
}
// WithMetadata for the generated account.
func WithMetadata(md map[string]string) GenerateOption {
return func(o *GenerateOptions) {
o.Metadata = md
}
}
// WithProvider for the generated account.
func WithProvider(p string) GenerateOption {
return func(o *GenerateOptions) {
o.Provider = p
}
}
// WithScopes for the generated account.
func WithScopes(s ...string) GenerateOption {
return func(o *GenerateOptions) {
o.Scopes = s
}
}
// NewGenerateOptions from a slice of options.
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
var options GenerateOptions
for _, o := range opts {
o(&options)
}
return options
}
type TokenOptions struct {
// ID for the account
ID string
// Secret for the account
Secret string
// RefreshToken is used to refesh a token
RefreshToken string
// Expiry is the time the token should live for
Expiry time.Duration
}
type TokenOption func(o *TokenOptions)
// WithExpiry for the token.
func WithExpiry(ex time.Duration) TokenOption {
return func(o *TokenOptions) {
o.Expiry = ex
}
}
func WithCredentials(id, secret string) TokenOption {
return func(o *TokenOptions) {
o.ID = id
o.Secret = secret
}
}
func WithToken(rt string) TokenOption {
return func(o *TokenOptions) {
o.RefreshToken = rt
}
}
// NewTokenOptions from a slice of options.
func NewTokenOptions(opts ...TokenOption) TokenOptions {
var options TokenOptions
for _, o := range opts {
o(&options)
}
// set default expiry of token
if options.Expiry == 0 {
options.Expiry = time.Minute
}
return options
}
type VerifyOptions struct {
Context context.Context
}
type VerifyOption func(o *VerifyOptions)
func VerifyContext(ctx context.Context) VerifyOption {
return func(o *VerifyOptions) {
o.Context = ctx
}
}
type ListOptions struct {
Context context.Context
}
type ListOption func(o *ListOptions)
func RulesContext(ctx context.Context) ListOption {
return func(o *ListOptions) {
o.Context = ctx
}
}
-91
View File
@@ -1,91 +0,0 @@
package auth
import (
"fmt"
"sort"
"strings"
)
// Verify an account has access to a resource using the rules provided. If the account does not have
// access an error will be returned. If there are no rules provided which match the resource, an error
// will be returned.
func Verify(rules []*Rule, acc *Account, res *Resource) error {
// the rule is only to be applied if the type matches the resource or is catch-all (*)
validTypes := []string{"*", res.Type}
// the rule is only to be applied if the name matches the resource or is catch-all (*)
validNames := []string{"*", res.Name}
// rules can have wildcard excludes on endpoints since this can also be a path for web services,
// e.g. /foo/* would include /foo/bar. We also want to check for wildcards and the exact endpoint
validEndpoints := []string{"*", res.Endpoint}
if comps := strings.Split(res.Endpoint, "/"); len(comps) > 1 {
for i := 1; i < len(comps)+1; i++ {
wildcard := fmt.Sprintf("%v/*", strings.Join(comps[0:i], "/"))
validEndpoints = append(validEndpoints, wildcard)
}
}
// filter the rules to the ones which match the criteria above
filteredRules := make([]*Rule, 0)
for _, rule := range rules {
if !include(validTypes, rule.Resource.Type) {
continue
}
if !include(validNames, rule.Resource.Name) {
continue
}
if !include(validEndpoints, rule.Resource.Endpoint) {
continue
}
filteredRules = append(filteredRules, rule)
}
// sort the filtered rules by priority, highest to lowest
sort.SliceStable(filteredRules, func(i, j int) bool {
return filteredRules[i].Priority > filteredRules[j].Priority
})
// loop through the rules and check for a rule which applies to this account
for _, rule := range filteredRules {
// a blank scope indicates the rule applies to everyone, even nil accounts
if rule.Scope == ScopePublic && rule.Access == AccessDenied {
return ErrForbidden
} else if rule.Scope == ScopePublic && rule.Access == AccessGranted {
return nil
}
// all further checks require an account
if acc == nil {
continue
}
// this rule applies to any account
if rule.Scope == ScopeAccount && rule.Access == AccessDenied {
return ErrForbidden
} else if rule.Scope == ScopeAccount && rule.Access == AccessGranted {
return nil
}
// if the account has the necessary scope
if include(acc.Scopes, rule.Scope) && rule.Access == AccessDenied {
return ErrForbidden
} else if include(acc.Scopes, rule.Scope) && rule.Access == AccessGranted {
return nil
}
}
// if no rules matched then return forbidden
return ErrForbidden
}
// include is a helper function which checks to see if the slice contains the value. includes is
// not case sensitive.
func include(slice []string, val string) bool {
for _, s := range slice {
if strings.EqualFold(s, val) {
return true
}
}
return false
}
-288
View File
@@ -1,288 +0,0 @@
package auth
import (
"testing"
)
func TestVerify(t *testing.T) {
srvResource := &Resource{
Type: "service",
Name: "go.micro.service.foo",
Endpoint: "Foo.Bar",
}
webResource := &Resource{
Type: "service",
Name: "go.micro.web.foo",
Endpoint: "/foo/bar",
}
catchallResource := &Resource{
Type: "*",
Name: "*",
Endpoint: "*",
}
tt := []struct {
Name string
Rules []*Rule
Account *Account
Resource *Resource
Error error
}{
{
Name: "NoRules",
Rules: []*Rule{},
Account: nil,
Resource: srvResource,
Error: ErrForbidden,
},
{
Name: "CatchallPublicAccount",
Account: &Account{},
Resource: srvResource,
Rules: []*Rule{
{
Scope: "",
Resource: catchallResource,
},
},
},
{
Name: "CatchallPublicNoAccount",
Resource: srvResource,
Rules: []*Rule{
{
Scope: "",
Resource: catchallResource,
},
},
},
{
Name: "CatchallPrivateAccount",
Account: &Account{},
Resource: srvResource,
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
},
},
},
{
Name: "CatchallPrivateNoAccount",
Resource: srvResource,
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
},
},
Error: ErrForbidden,
},
{
Name: "CatchallServiceRuleMatch",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: srvResource.Type,
Name: srvResource.Name,
Endpoint: "*",
},
},
},
},
{
Name: "CatchallServiceRuleNoMatch",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: srvResource.Type,
Name: "wrongname",
Endpoint: "*",
},
},
},
Error: ErrForbidden,
},
{
Name: "ExactRuleValidScope",
Resource: srvResource,
Account: &Account{
Scopes: []string{"neededscope"},
},
Rules: []*Rule{
{
Scope: "neededscope",
Resource: srvResource,
},
},
},
{
Name: "ExactRuleInvalidScope",
Resource: srvResource,
Account: &Account{
Scopes: []string{"neededscope"},
},
Rules: []*Rule{
{
Scope: "invalidscope",
Resource: srvResource,
},
},
Error: ErrForbidden,
},
{
Name: "CatchallDenyWithAccount",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
},
},
Error: ErrForbidden,
},
{
Name: "CatchallDenyWithNoAccount",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
},
},
Error: ErrForbidden,
},
{
Name: "RulePriorityGrantFirst",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessGranted,
Priority: 1,
},
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
Priority: 0,
},
},
},
{
Name: "RulePriorityDenyFirst",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessGranted,
Priority: 0,
},
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
Priority: 1,
},
},
Error: ErrForbidden,
},
{
Name: "WebExactEndpointValid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: webResource,
},
},
},
{
Name: "WebExactEndpointInalid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "invalidendpoint",
},
},
},
Error: ErrForbidden,
},
{
Name: "WebWildcardEndpoint",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "*",
},
},
},
},
{
Name: "WebWildcardPathEndpointValid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "/foo/*",
},
},
},
},
{
Name: "WebWildcardPathEndpointInvalid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "/bar/*",
},
},
},
Error: ErrForbidden,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
if err := Verify(tc.Rules, tc.Account, tc.Resource); err != tc.Error {
t.Errorf("Expected %v but got %v", tc.Error, err)
}
})
}
}
+7 -7
View File
@@ -18,21 +18,19 @@ type Broker interface {
// message and optional Ack method to acknowledge receipt of the message.
type Handler func(Event) error
// Message is a message send/received from the broker.
type Message struct {
Header map[string]string
Body []byte
}
// Event is given to a subscription handler for processing.
// Event is given to a subscription handler for processing
type Event interface {
Topic() string
Message() *Message
Ack() error
Error() error
}
// Subscriber is a convenience return type for the Subscribe method.
// Subscriber is a convenience return type for the Subscribe method
type Subscriber interface {
Options() SubscribeOptions
Topic() string
@@ -40,10 +38,13 @@ type Subscriber interface {
}
var (
// DefaultBroker is the default Broker.
DefaultBroker = NewHttpBroker()
DefaultBroker Broker = newHttpBroker()
)
func NewBroker(opts ...Option) Broker {
return newHttpBroker(opts...)
}
func Init(opts ...Option) error {
return DefaultBroker.Init(opts...)
}
@@ -64,7 +65,6 @@ func Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscrib
return DefaultBroker.Subscribe(topic, handler, opts...)
}
// String returns the name of the Broker.
func String() string {
return DefaultBroker.String()
}
+5 -16
View File
@@ -1,13 +1,13 @@
package test
package broker
import (
"go-micro.dev/v5/registry"
"github.com/micro/go-micro/registry"
)
var (
// Data is a set of mock registry data.
Data = map[string][]*registry.Service{
"foo": {
// mock data
testData = map[string][]*registry.Service{
"foo": []*registry.Service{
{
Name: "foo",
Version: "1.0.0",
@@ -45,14 +45,3 @@ var (
},
}
)
// EmptyChannel will empty out a error channel by checking if an error is
// present, and if so return the error.
func EmptyChannel(c chan error) error {
select {
case err := <-c:
return err
default:
return nil
}
}
+11
View File
@@ -0,0 +1,11 @@
// Package http provides a http based message broker
package http
import (
"github.com/micro/go-micro/broker"
)
// NewBroker returns a new http broker
func NewBroker(opts ...broker.Option) broker.Broker {
return broker.NewBroker(opts...)
}
+23
View File
@@ -0,0 +1,23 @@
package http
import (
"context"
"net/http"
"github.com/micro/go-micro/broker"
)
// Handle registers the handler for the given pattern.
func Handle(pattern string, handler http.Handler) broker.Option {
return func(o *broker.Options) {
if o.Context == nil {
o.Context = context.Background()
}
handlers, ok := o.Context.Value("http_handlers").(map[string]http.Handler)
if !ok {
handlers = make(map[string]http.Handler)
}
handlers[pattern] = handler
o.Context = context.WithValue(o.Context, "http_handlers", handlers)
}
}
+72 -99
View File
@@ -2,10 +2,12 @@ package broker
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
@@ -15,71 +17,67 @@ import (
"time"
"github.com/google/uuid"
"go-micro.dev/v5/codec/json"
merr "go-micro.dev/v5/errors"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/cache"
"go-micro.dev/v5/transport/headers"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
mls "go-micro.dev/v5/util/tls"
"github.com/micro/go-micro/codec/json"
merr "github.com/micro/go-micro/errors"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/cache"
maddr "github.com/micro/go-micro/util/addr"
mnet "github.com/micro/go-micro/util/net"
mls "github.com/micro/go-micro/util/tls"
"golang.org/x/net/http2"
)
// HTTP Broker is a point to point async broker.
// HTTP Broker is a point to point async broker
type httpBroker struct {
opts Options
r registry.Registry
id string
address string
opts Options
mux *http.ServeMux
c *http.Client
subscribers map[string][]*httpSubscriber
exit chan chan error
inbox map[string][][]byte
id string
address string
c *http.Client
r registry.Registry
sync.RWMutex
subscribers map[string][]*httpSubscriber
running bool
exit chan chan error
// offline message inbox
mtx sync.RWMutex
running bool
mtx sync.RWMutex
inbox map[string][][]byte
}
type httpSubscriber struct {
opts SubscribeOptions
id string
topic string
fn Handler
svc *registry.Service
hb *httpBroker
id string
topic string
}
type httpEvent struct {
err error
m *Message
t string
m *Message
t string
}
var (
DefaultPath = "/"
DefaultAddress = "127.0.0.1:0"
serviceName = "micro.http.broker"
DefaultSubPath = "/_sub"
broadcastVersion = "ff.http.broadcast"
registerTTL = time.Minute
registerInterval = time.Second * 30
)
func init() {
rand.Seed(time.Now().Unix())
}
func newTransport(config *tls.Config) *http.Transport {
if config == nil {
// Use environment-based config - secure by default
config = mls.Config()
config = &tls.Config{
InsecureSkipVerify: true,
}
}
dialTLS := func(network string, addr string) (net.Conn, error) {
@@ -106,27 +104,32 @@ func newTransport(config *tls.Config) *http.Transport {
}
func newHttpBroker(opts ...Option) Broker {
options := *NewOptions(opts...)
options.Registry = registry.DefaultRegistry
options.Codec = json.Marshaler{}
options := Options{
Codec: json.Marshaler{},
Context: context.TODO(),
}
for _, o := range opts {
o(&options)
}
// set address
addr := DefaultAddress
addr := ":0"
if len(options.Addrs) > 0 && len(options.Addrs[0]) > 0 {
addr = options.Addrs[0]
}
// get registry
reg, ok := options.Context.Value(registryKey).(registry.Registry)
if !ok {
reg = registry.DefaultRegistry
}
h := &httpBroker{
id: uuid.New().String(),
id: "broker-" + uuid.New().String(),
address: addr,
opts: options,
r: options.Registry,
r: reg,
c: &http.Client{Transport: newTransport(options.TLSConfig)},
subscribers: make(map[string][]*httpSubscriber),
exit: make(chan chan error),
@@ -135,7 +138,7 @@ func newHttpBroker(opts ...Option) Broker {
}
// specify the message handler
h.mux.Handle(DefaultPath, h)
h.mux.Handle(DefaultSubPath, h)
// get optional handlers
if h.opts.Context != nil {
@@ -154,10 +157,6 @@ func (h *httpEvent) Ack() error {
return nil
}
func (h *httpEvent) Error() error {
return h.err
}
func (h *httpEvent) Message() *Message {
return h.m
}
@@ -237,13 +236,12 @@ func (h *httpBroker) unsubscribe(s *httpSubscriber) error {
h.Lock()
defer h.Unlock()
//nolint:prealloc
var subscribers []*httpSubscriber
// look for subscriber
for _, sub := range h.subscribers[s.topic] {
// deregister and skip forward
if sub == s {
if sub.id == s.id {
_ = h.r.Deregister(sub.svc)
continue
}
@@ -297,7 +295,7 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
req.ParseForm()
b, err := io.ReadAll(req.Body)
b, err := ioutil.ReadAll(req.Body)
if err != nil {
errr := merr.InternalServerError("go.micro.broker", "Error reading request body: %v", err)
w.WriteHeader(500)
@@ -313,8 +311,8 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
topic := m.Header[headers.Message]
// delete(m.Header, ":topic")
topic := m.Header[":topic"]
delete(m.Header, ":topic")
if len(topic) == 0 {
errr := merr.InternalServerError("go.micro.broker", "Topic not found")
@@ -326,22 +324,15 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
p := &httpEvent{m: m, t: topic}
id := req.Form.Get("id")
//nolint:prealloc
var subs []Handler
h.RLock()
for _, subscriber := range h.subscribers[topic] {
if id != subscriber.id {
continue
if id == subscriber.id {
// sub is sync; crufty rate limiting
// so we don't hose the cpu
subscriber.fn(p)
}
subs = append(subs, subscriber.fn)
}
h.RUnlock()
// execute the handler
for _, fn := range subs {
p.err = fn(p)
}
}
func (h *httpBroker) Address() string {
@@ -416,8 +407,8 @@ func (h *httpBroker) Connect() error {
}()
// get registry
reg := h.opts.Registry
if reg == nil {
reg, ok := h.opts.Context.Value(registryKey).(registry.Registry)
if !ok {
reg = registry.DefaultRegistry
}
// set cache
@@ -429,6 +420,7 @@ func (h *httpBroker) Connect() error {
}
func (h *httpBroker) Disconnect() error {
h.RLock()
if !h.running {
h.RUnlock()
@@ -475,12 +467,12 @@ func (h *httpBroker) Init(opts ...Option) error {
}
if len(h.id) == 0 {
h.id = "go.micro.http.broker-" + uuid.New().String()
h.id = "broker-" + uuid.New().String()
}
// get registry
reg := h.opts.Registry
if reg == nil {
reg, ok := h.opts.Context.Value(registryKey).(registry.Registry)
if !ok {
reg = registry.DefaultRegistry
}
@@ -517,7 +509,7 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption)
m.Header[k] = v
}
m.Header[headers.Message] = topic
m.Header[":topic"] = topic
// encode the message
b, err := h.opts.Codec.Marshal(m)
@@ -530,10 +522,11 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption)
// now attempt to get the service
h.RLock()
s, err := h.r.GetService(serviceName)
s, err := h.r.GetService("topic:" + topic)
if err != nil {
h.RUnlock()
return err
// ignore error
return nil
}
h.RUnlock()
@@ -548,38 +541,22 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption)
vals := url.Values{}
vals.Add("id", node.Id)
uri := fmt.Sprintf("%s://%s%s?%s", scheme, node.Address, DefaultPath, vals.Encode())
uri := fmt.Sprintf("%s://%s%s?%s", scheme, node.Address, DefaultSubPath, vals.Encode())
r, err := h.c.Post(uri, "application/json", bytes.NewReader(b))
if err != nil {
return err
}
// discard response body
io.Copy(io.Discard, r.Body)
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
return nil
}
srv := func(s []*registry.Service, b []byte) {
for _, service := range s {
var nodes []*registry.Node
for _, node := range service.Nodes {
// only use nodes tagged with broker http
if node.Metadata["broker"] != "http" {
continue
}
// look for nodes for the topic
if node.Metadata["topic"] != topic {
continue
}
nodes = append(nodes, node)
}
// only process if we have nodes
if len(nodes) == 0 {
if len(service.Nodes) == 0 {
continue
}
@@ -589,7 +566,7 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption)
var success bool
// publish to all nodes
for _, node := range nodes {
for _, node := range service.Nodes {
// publish async
if err := pub(node, topic, b); err == nil {
success = true
@@ -602,7 +579,7 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption)
}
default:
// select node to publish to
node := nodes[rand.Int()%len(nodes)]
node := service.Nodes[rand.Int()%len(service.Nodes)]
// publish async to one node
if err := pub(node, topic, b); err != nil {
@@ -650,6 +627,9 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO
return nil, err
}
// create unique id
id := h.id + "." + uuid.New().String()
var secure bool
if h.opts.Secure || h.opts.TLSConfig != nil {
@@ -658,12 +638,10 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO
// register service
node := &registry.Node{
Id: topic + "-" + h.id,
Id: id,
Address: mnet.HostPort(addr, port),
Metadata: map[string]string{
"secure": fmt.Sprintf("%t", secure),
"broker": "http",
"topic": topic,
},
}
@@ -674,7 +652,7 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO
}
service := &registry.Service{
Name: serviceName,
Name: "topic:" + topic,
Version: version,
Nodes: []*registry.Node{node},
}
@@ -683,7 +661,7 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO
subscriber := &httpSubscriber{
opts: options,
hb: h,
id: node.Id,
id: id,
topic: topic,
fn: handler,
svc: service,
@@ -701,8 +679,3 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO
func (h *httpBroker) String() string {
return "http"
}
// NewHttpBroker returns a new http broker.
func NewHttpBroker(opts ...Option) Broker {
return newHttpBroker(opts...)
}
@@ -1,138 +1,104 @@
package broker_test
package broker
import (
"sync"
"testing"
"time"
glog "github.com/go-log/log"
"github.com/google/uuid"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/registry"
"github.com/micro/go-micro/registry/memory"
"github.com/micro/go-micro/util/log"
)
var (
// mock data.
testData = map[string][]*registry.Service{
"foo": {
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-1.0.0-123",
Address: "localhost:9999",
},
{
Id: "foo-1.0.0-321",
Address: "localhost:9999",
},
},
},
{
Name: "foo",
Version: "1.0.1",
Nodes: []*registry.Node{
{
Id: "foo-1.0.1-321",
Address: "localhost:6666",
},
},
},
{
Name: "foo",
Version: "1.0.3",
Nodes: []*registry.Node{
{
Id: "foo-1.0.3-345",
Address: "localhost:8888",
},
},
},
},
}
)
func newTestRegistry() registry.Registry {
return registry.NewMemoryRegistry(registry.Services(testData))
func newTestRegistry() *memory.Registry {
r := memory.NewRegistry()
m := r.(*memory.Registry)
m.Services = testData
return m
}
func sub(b *testing.B, c int) {
b.StopTimer()
func sub(be *testing.B, c int) {
// set no op logger
log.SetLogger(glog.DefaultLogger)
be.StopTimer()
m := newTestRegistry()
brker := broker.NewHttpBroker(broker.Registry(m))
b := NewBroker(Registry(m))
topic := uuid.New().String()
if err := brker.Init(); err != nil {
b.Fatalf("Unexpected init error: %v", err)
if err := b.Init(); err != nil {
be.Fatalf("Unexpected init error: %v", err)
}
if err := brker.Connect(); err != nil {
b.Fatalf("Unexpected connect error: %v", err)
if err := b.Connect(); err != nil {
be.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
msg := &Message{
Header: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"message": "Hello World"}`),
}
var subs []broker.Subscriber
var subs []Subscriber
done := make(chan bool, c)
for i := 0; i < c; i++ {
sub, err := brker.Subscribe(topic, func(p broker.Event) error {
sub, err := b.Subscribe(topic, func(p Event) error {
done <- true
m := p.Message()
if string(m.Body) != string(msg.Body) {
b.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
be.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
}
return nil
}, broker.Queue("shared"))
}, Queue("shared"))
if err != nil {
b.Fatalf("Unexpected subscribe error: %v", err)
be.Fatalf("Unexpected subscribe error: %v", err)
}
subs = append(subs, sub)
}
for i := 0; i < b.N; i++ {
b.StartTimer()
if err := brker.Publish(topic, msg); err != nil {
b.Fatalf("Unexpected publish error: %v", err)
for i := 0; i < be.N; i++ {
be.StartTimer()
if err := b.Publish(topic, msg); err != nil {
be.Fatalf("Unexpected publish error: %v", err)
}
<-done
b.StopTimer()
be.StopTimer()
}
for _, sub := range subs {
if err := sub.Unsubscribe(); err != nil {
b.Fatalf("Unexpected unsubscribe error: %v", err)
}
sub.Unsubscribe()
}
if err := brker.Disconnect(); err != nil {
b.Fatalf("Unexpected disconnect error: %v", err)
if err := b.Disconnect(); err != nil {
be.Fatalf("Unexpected disconnect error: %v", err)
}
}
func pub(b *testing.B, c int) {
b.StopTimer()
func pub(be *testing.B, c int) {
// set no op logger
log.SetLogger(glog.DefaultLogger)
be.StopTimer()
m := newTestRegistry()
brk := broker.NewHttpBroker(broker.Registry(m))
b := NewBroker(Registry(m))
topic := uuid.New().String()
if err := brk.Init(); err != nil {
b.Fatalf("Unexpected init error: %v", err)
if err := b.Init(); err != nil {
be.Fatalf("Unexpected init error: %v", err)
}
if err := brk.Connect(); err != nil {
b.Fatalf("Unexpected connect error: %v", err)
if err := b.Connect(); err != nil {
be.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
msg := &Message{
Header: map[string]string{
"Content-Type": "application/json",
},
@@ -141,27 +107,27 @@ func pub(b *testing.B, c int) {
done := make(chan bool, c*4)
sub, err := brk.Subscribe(topic, func(p broker.Event) error {
sub, err := b.Subscribe(topic, func(p Event) error {
done <- true
m := p.Message()
if string(m.Body) != string(msg.Body) {
b.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
be.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
}
return nil
}, broker.Queue("shared"))
}, Queue("shared"))
if err != nil {
b.Fatalf("Unexpected subscribe error: %v", err)
be.Fatalf("Unexpected subscribe error: %v", err)
}
var wg sync.WaitGroup
ch := make(chan int, c*4)
b.StartTimer()
be.StartTimer()
for i := 0; i < c; i++ {
go func() {
for range ch {
if err := brk.Publish(topic, msg); err != nil {
b.Fatalf("Unexpected publish error: %v", err)
for _ = range ch {
if err := b.Publish(topic, msg); err != nil {
be.Fatalf("Unexpected publish error: %v", err)
}
select {
case <-done:
@@ -172,25 +138,25 @@ func pub(b *testing.B, c int) {
}()
}
for i := 0; i < b.N; i++ {
for i := 0; i < be.N; i++ {
wg.Add(1)
ch <- i
}
wg.Wait()
b.StopTimer()
be.StopTimer()
sub.Unsubscribe()
close(ch)
close(done)
if err := brk.Disconnect(); err != nil {
b.Fatalf("Unexpected disconnect error: %v", err)
if err := b.Disconnect(); err != nil {
be.Fatalf("Unexpected disconnect error: %v", err)
}
}
func TestBroker(t *testing.T) {
m := newTestRegistry()
b := broker.NewHttpBroker(broker.Registry(m))
b := NewBroker(Registry(m))
if err := b.Init(); err != nil {
t.Fatalf("Unexpected init error: %v", err)
@@ -200,7 +166,7 @@ func TestBroker(t *testing.T) {
t.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
msg := &Message{
Header: map[string]string{
"Content-Type": "application/json",
},
@@ -209,7 +175,7 @@ func TestBroker(t *testing.T) {
done := make(chan bool)
sub, err := b.Subscribe("test", func(p broker.Event) error {
sub, err := b.Subscribe("test", func(p Event) error {
m := p.Message()
if string(m.Body) != string(msg.Body) {
@@ -228,9 +194,7 @@ func TestBroker(t *testing.T) {
}
<-done
if err := sub.Unsubscribe(); err != nil {
t.Fatalf("Unexpected unsubscribe error: %v", err)
}
sub.Unsubscribe()
if err := b.Disconnect(); err != nil {
t.Fatalf("Unexpected disconnect error: %v", err)
@@ -239,7 +203,7 @@ func TestBroker(t *testing.T) {
func TestConcurrentSubBroker(t *testing.T) {
m := newTestRegistry()
b := broker.NewHttpBroker(broker.Registry(m))
b := NewBroker(Registry(m))
if err := b.Init(); err != nil {
t.Fatalf("Unexpected init error: %v", err)
@@ -249,18 +213,18 @@ func TestConcurrentSubBroker(t *testing.T) {
t.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
msg := &Message{
Header: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"message": "Hello World"}`),
}
var subs []broker.Subscriber
var subs []Subscriber
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
sub, err := b.Subscribe("test", func(p broker.Event) error {
sub, err := b.Subscribe("test", func(p Event) error {
defer wg.Done()
m := p.Message()
@@ -286,9 +250,7 @@ func TestConcurrentSubBroker(t *testing.T) {
wg.Wait()
for _, sub := range subs {
if err := sub.Unsubscribe(); err != nil {
t.Fatalf("Unexpected unsubscribe error: %v", err)
}
sub.Unsubscribe()
}
if err := b.Disconnect(); err != nil {
@@ -298,7 +260,7 @@ func TestConcurrentSubBroker(t *testing.T) {
func TestConcurrentPubBroker(t *testing.T) {
m := newTestRegistry()
b := broker.NewHttpBroker(broker.Registry(m))
b := NewBroker(Registry(m))
if err := b.Init(); err != nil {
t.Fatalf("Unexpected init error: %v", err)
@@ -308,7 +270,7 @@ func TestConcurrentPubBroker(t *testing.T) {
t.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
msg := &Message{
Header: map[string]string{
"Content-Type": "application/json",
},
@@ -317,7 +279,7 @@ func TestConcurrentPubBroker(t *testing.T) {
var wg sync.WaitGroup
sub, err := b.Subscribe("test", func(p broker.Event) error {
sub, err := b.Subscribe("test", func(p Event) error {
defer wg.Done()
m := p.Message()
@@ -342,9 +304,7 @@ func TestConcurrentPubBroker(t *testing.T) {
wg.Wait()
if err := sub.Unsubscribe(); err != nil {
t.Fatalf("Unexpected unsubscribe error: %v", err)
}
sub.Unsubscribe()
if err := b.Disconnect(); err != nil {
t.Fatalf("Unexpected disconnect error: %v", err)
@@ -362,6 +322,14 @@ func BenchmarkSub32(b *testing.B) {
sub(b, 32)
}
func BenchmarkSub64(b *testing.B) {
sub(b, 64)
}
func BenchmarkSub128(b *testing.B) {
sub(b, 128)
}
func BenchmarkPub1(b *testing.B) {
pub(b, 1)
}
@@ -373,3 +341,11 @@ func BenchmarkPub8(b *testing.B) {
func BenchmarkPub32(b *testing.B) {
pub(b, 32)
}
func BenchmarkPub64(b *testing.B) {
pub(b, 64)
}
func BenchmarkPub128(b *testing.B) {
pub(b, 128)
}
+30 -63
View File
@@ -1,44 +1,42 @@
// Package memory provides a memory broker
package broker
package memory
import (
"errors"
"math/rand"
"sync"
"time"
"github.com/google/uuid"
log "go-micro.dev/v5/logger"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
"github.com/micro/go-micro/broker"
maddr "github.com/micro/go-micro/util/addr"
mnet "github.com/micro/go-micro/util/net"
)
type memoryBroker struct {
opts *Options
Subscribers map[string][]*memorySubscriber
opts broker.Options
addr string
sync.RWMutex
connected bool
connected bool
Subscribers map[string][]*memorySubscriber
}
type memoryEvent struct {
err error
message interface{}
opts *Options
topic string
message *broker.Message
}
type memorySubscriber struct {
opts SubscribeOptions
exit chan bool
handler Handler
id string
topic string
exit chan bool
handler broker.Handler
opts broker.SubscribeOptions
}
func (m *memoryBroker) Options() Options {
return *m.opts
func (m *memoryBroker) Options() broker.Options {
return m.opts
}
func (m *memoryBroker) Address() string {
@@ -53,8 +51,7 @@ func (m *memoryBroker) Connect() error {
return nil
}
// use 127.0.0.1 to avoid scan of all network interfaces
addr, err := maddr.Extract("127.0.0.1")
addr, err := maddr.Extract("::")
if err != nil {
return err
}
@@ -81,14 +78,14 @@ func (m *memoryBroker) Disconnect() error {
return nil
}
func (m *memoryBroker) Init(opts ...Option) error {
func (m *memoryBroker) Init(opts ...broker.Option) error {
for _, o := range opts {
o(m.opts)
o(&m.opts)
}
return nil
}
func (m *memoryBroker) Publish(topic string, msg *Message, opts ...PublishOption) error {
func (m *memoryBroker) Publish(topic string, message *broker.Message, opts ...broker.PublishOption) error {
m.RLock()
if !m.connected {
m.RUnlock()
@@ -101,30 +98,13 @@ func (m *memoryBroker) Publish(topic string, msg *Message, opts ...PublishOption
return nil
}
var v interface{}
if m.opts.Codec != nil {
buf, err := m.opts.Codec.Marshal(msg)
if err != nil {
return err
}
v = buf
} else {
v = msg
}
p := &memoryEvent{
topic: topic,
message: v,
opts: m.opts,
message: message,
}
for _, sub := range subs {
if err := sub.handler(p); err != nil {
p.err = err
if eh := m.opts.ErrorHandler; eh != nil {
eh(p)
continue
}
return err
}
}
@@ -132,7 +112,7 @@ func (m *memoryBroker) Publish(topic string, msg *Message, opts ...PublishOption
return nil
}
func (m *memoryBroker) Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
func (m *memoryBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
m.RLock()
if !m.connected {
m.RUnlock()
@@ -140,7 +120,7 @@ func (m *memoryBroker) Subscribe(topic string, handler Handler, opts ...Subscrib
}
m.RUnlock()
var options SubscribeOptions
var options broker.SubscribeOptions
for _, o := range opts {
o(&options)
}
@@ -182,31 +162,15 @@ func (m *memoryEvent) Topic() string {
return m.topic
}
func (m *memoryEvent) Message() *Message {
switch v := m.message.(type) {
case *Message:
return v
case []byte:
msg := &Message{}
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
m.opts.Logger.Logf(log.ErrorLevel, "[memory]: failed to unmarshal: %v\n", err)
return nil
}
return msg
}
return nil
func (m *memoryEvent) Message() *broker.Message {
return m.message
}
func (m *memoryEvent) Ack() error {
return nil
}
func (m *memoryEvent) Error() error {
return m.err
}
func (m *memorySubscriber) Options() SubscribeOptions {
func (m *memorySubscriber) Options() broker.SubscribeOptions {
return m.opts
}
@@ -219,9 +183,12 @@ func (m *memorySubscriber) Unsubscribe() error {
return nil
}
func NewMemoryBroker(opts ...Option) Broker {
options := NewOptions(opts...)
func NewBroker(opts ...broker.Option) broker.Broker {
var options broker.Options
rand.Seed(time.Now().UnixNano())
for _, o := range opts {
o(&options)
}
return &memoryBroker{
opts: options,
@@ -1,14 +1,14 @@
package broker_test
package memory
import (
"fmt"
"testing"
"go-micro.dev/v5/broker"
"github.com/micro/go-micro/broker"
)
func TestMemoryBroker(t *testing.T) {
b := broker.NewMemoryBroker()
b := NewBroker()
if err := b.Connect(); err != nil {
t.Fatalf("Unexpected connect error %v", err)
+22 -2
View File
@@ -3,10 +3,20 @@ package nats
import (
"context"
"go-micro.dev/v5/broker"
"github.com/micro/go-micro/broker"
)
// setBrokerOption returns a function to setup a context with given value.
// setSubscribeOption returns a function to setup a context with given value
func setSubscribeOption(k, v interface{}) broker.SubscribeOption {
return func(o *broker.SubscribeOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// setBrokerOption returns a function to setup a context with given value
func setBrokerOption(k, v interface{}) broker.Option {
return func(o *broker.Options) {
if o.Context == nil {
@@ -15,3 +25,13 @@ func setBrokerOption(k, v interface{}) broker.Option {
o.Context = context.WithValue(o.Context, k, v)
}
}
// setPublishOption returns a function to setup a context with given value
func setPublishOption(k, v interface{}) broker.PublishOption {
return func(o *broker.PublishOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
+44 -104
View File
@@ -7,39 +7,30 @@ import (
"strings"
"sync"
natsp "github.com/nats-io/nats.go"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/codec/json"
"go-micro.dev/v5/logger"
"go-micro.dev/v5/registry"
"github.com/micro/go-micro/broker"
"github.com/micro/go-micro/codec/json"
nats "github.com/nats-io/nats.go"
)
type natsBroker struct {
sync.Once
sync.RWMutex
// indicate if we're connected
connected bool
addrs []string
conn *natsp.Conn
opts broker.Options
nopts natsp.Options
// should we drain the connection
addrs []string
conn *nats.Conn
opts broker.Options
nopts nats.Options
drain bool
closeCh chan (error)
}
type subscriber struct {
s *natsp.Subscription
s *nats.Subscription
opts broker.SubscribeOptions
}
type publication struct {
t string
err error
m *broker.Message
t string
m *broker.Message
}
func (p *publication) Topic() string {
@@ -55,10 +46,6 @@ func (p *publication) Ack() error {
return nil
}
func (p *publication) Error() error {
return p.err
}
func (s *subscriber) Options() broker.SubscribeOptions {
return s.opts
}
@@ -75,7 +62,6 @@ func (n *natsBroker) Address() string {
if n.conn != nil && n.conn.IsConnected() {
return n.conn.ConnectedUrl()
}
if len(n.addrs) > 0 {
return n.addrs[0]
}
@@ -83,8 +69,7 @@ func (n *natsBroker) Address() string {
return ""
}
func (n *natsBroker) setAddrs(addrs []string) []string {
//nolint:prealloc
func setAddrs(addrs []string) []string {
var cAddrs []string
for _, addr := range addrs {
if len(addr) == 0 {
@@ -96,7 +81,7 @@ func (n *natsBroker) setAddrs(addrs []string) []string {
cAddrs = append(cAddrs, addr)
}
if len(cAddrs) == 0 {
cAddrs = []string{natsp.DefaultURL}
cAddrs = []string{nats.DefaultURL}
}
return cAddrs
}
@@ -105,18 +90,13 @@ func (n *natsBroker) Connect() error {
n.Lock()
defer n.Unlock()
if n.connected {
return nil
}
status := natsp.CLOSED
status := nats.CLOSED
if n.conn != nil {
status = n.conn.Status()
}
switch status {
case natsp.CONNECTED, natsp.RECONNECTING, natsp.CONNECTING:
n.connected = true
case nats.CONNECTED, nats.RECONNECTING, nats.CONNECTING:
return nil
default: // DISCONNECTED or CLOSED or DRAINING
opts := n.nopts
@@ -134,27 +114,18 @@ func (n *natsBroker) Connect() error {
return err
}
n.conn = c
n.connected = true
return nil
}
}
func (n *natsBroker) Disconnect() error {
n.Lock()
defer n.Unlock()
// drain the connection if specified
n.RLock()
defer n.RUnlock()
if n.drain {
n.conn.Drain()
n.closeCh <- nil
return <-n.closeCh
}
// close the client connection
n.conn.Close()
// set not connected
n.connected = false
return nil
}
@@ -168,27 +139,19 @@ func (n *natsBroker) Options() broker.Options {
}
func (n *natsBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error {
n.RLock()
defer n.RUnlock()
if n.conn == nil {
return errors.New("not connected")
}
b, err := n.opts.Codec.Marshal(msg)
if err != nil {
return err
}
n.RLock()
defer n.RUnlock()
return n.conn.Publish(topic, b)
}
func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
n.RLock()
if n.conn == nil {
n.RUnlock()
return nil, errors.New("not connected")
}
n.RUnlock()
opt := broker.SubscribeOptions{
AutoAck: true,
@@ -199,31 +162,15 @@ func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...bro
o(&opt)
}
fn := func(msg *natsp.Msg) {
fn := func(msg *nats.Msg) {
var m broker.Message
pub := &publication{t: msg.Subject}
eh := n.opts.ErrorHandler
err := n.opts.Codec.Unmarshal(msg.Data, &m)
pub.err = err
pub.m = &m
if err != nil {
m.Body = msg.Data
n.opts.Logger.Log(logger.ErrorLevel, err)
if eh != nil {
eh(pub)
}
if err := n.opts.Codec.Unmarshal(msg.Data, &m); err != nil {
return
}
if err := handler(pub); err != nil {
pub.err = err
n.opts.Logger.Log(logger.ErrorLevel, err)
if eh != nil {
eh(pub)
}
}
handler(&publication{m: &m, t: msg.Subject})
}
var sub *natsp.Subscription
var sub *nats.Subscription
var err error
n.RLock()
@@ -243,16 +190,31 @@ func (n *natsBroker) String() string {
return "nats"
}
func NewBroker(opts ...broker.Option) broker.Broker {
options := broker.Options{
// Default codec
Codec: json.Marshaler{},
Context: context.Background(),
}
n := &natsBroker{
opts: options,
}
n.setOption(opts...)
return n
}
func (n *natsBroker) setOption(opts ...broker.Option) {
for _, o := range opts {
o(&n.opts)
}
n.Once.Do(func() {
n.nopts = natsp.GetDefaultOptions()
n.nopts = nats.GetDefaultOptions()
})
if nopts, ok := n.opts.Context.Value(optionsKey{}).(natsp.Options); ok {
if nopts, ok := n.opts.Context.Value(optionsKey{}).(nats.Options); ok {
n.nopts = nopts
}
@@ -270,46 +232,24 @@ func (n *natsBroker) setOption(opts ...broker.Option) {
if n.opts.TLSConfig == nil {
n.opts.TLSConfig = n.nopts.TLSConfig
}
n.addrs = n.setAddrs(n.opts.Addrs)
n.addrs = setAddrs(n.opts.Addrs)
if n.opts.Context.Value(drainConnectionKey{}) != nil {
n.drain = true
n.closeCh = make(chan error)
n.nopts.ClosedCB = n.onClose
n.nopts.AsyncErrorCB = n.onAsyncError
n.nopts.DisconnectedErrCB = n.onDisconnectedError
}
}
func (n *natsBroker) onClose(conn *natsp.Conn) {
func (n *natsBroker) onClose(conn *nats.Conn) {
n.closeCh <- nil
}
func (n *natsBroker) onAsyncError(conn *natsp.Conn, sub *natsp.Subscription, err error) {
func (n *natsBroker) onAsyncError(conn *nats.Conn, sub *nats.Subscription, err error) {
// There are kinds of different async error nats might callback, but we are interested
// in ErrDrainTimeout only here.
if err == natsp.ErrDrainTimeout {
if err == nats.ErrDrainTimeout {
n.closeCh <- err
}
}
func (n *natsBroker) onDisconnectedError(conn *natsp.Conn, err error) {
n.closeCh <- err
}
func NewNatsBroker(opts ...broker.Option) broker.Broker {
options := broker.Options{
// Default codec
Codec: json.Marshaler{},
Context: context.Background(),
Registry: registry.DefaultRegistry,
Logger: logger.DefaultLogger,
}
n := &natsBroker{
opts: options,
}
n.setOption(opts...)
return n
}
+10 -7
View File
@@ -4,8 +4,8 @@ import (
"fmt"
"testing"
natsp "github.com/nats-io/nats.go"
"go-micro.dev/v5/broker"
"github.com/micro/go-micro/broker"
nats "github.com/nats-io/nats.go"
)
var addrTestCases = []struct {
@@ -45,8 +45,10 @@ var addrTestCases = []struct {
// TestInitAddrs tests issue #100. Ensures that if the addrs is set by an option in init it will be used.
func TestInitAddrs(t *testing.T) {
for _, tc := range addrTestCases {
t.Run(fmt.Sprintf("%s: %s", tc.name, tc.description), func(t *testing.T) {
var br broker.Broker
var addrs []string
@@ -57,19 +59,19 @@ func TestInitAddrs(t *testing.T) {
switch tc.name {
case "brokerOpts":
// we know that there are just two addrs in the dict
br = NewNatsBroker(broker.Addrs(addrs[0], addrs[1]))
br = NewBroker(broker.Addrs(addrs[0], addrs[1]))
br.Init()
case "brokerInit":
br = NewNatsBroker()
br = NewBroker()
// we know that there are just two addrs in the dict
br.Init(broker.Addrs(addrs[0], addrs[1]))
case "natsOpts":
nopts := natsp.GetDefaultOptions()
nopts := nats.GetDefaultOptions()
nopts.Servers = addrs
br = NewNatsBroker(Options(nopts))
br = NewBroker(Options(nopts))
br.Init()
case "default":
br = NewNatsBroker()
br = NewBroker()
br.Init()
}
@@ -92,5 +94,6 @@ func TestInitAddrs(t *testing.T) {
}
}
})
}
}
+5 -5
View File
@@ -1,19 +1,19 @@
package nats
import (
natsp "github.com/nats-io/nats.go"
"go-micro.dev/v5/broker"
"github.com/micro/go-micro/broker"
nats "github.com/nats-io/nats.go"
)
type optionsKey struct{}
type drainConnectionKey struct{}
// Options accepts nats.Options.
func Options(opts natsp.Options) broker.Option {
// Options accepts nats.Options
func Options(opts nats.Options) broker.Option {
return setBrokerOption(optionsKey{}, opts)
}
// DrainConnection will drain subscription on close.
// DrainConnection will drain subscription on close
func DrainConnection() broker.Option {
return setBrokerOption(drainConnectionKey{}, struct{}{})
}
+22 -66
View File
@@ -4,30 +4,18 @@ import (
"context"
"crypto/tls"
"go-micro.dev/v5/codec"
"go-micro.dev/v5/logger"
"go-micro.dev/v5/registry"
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/registry"
)
type Options struct {
Codec codec.Marshaler
// Logger is the underlying logger
Logger logger.Logger
// Registry used for clustering
Registry registry.Registry
Addrs []string
Secure bool
Codec codec.Marshaler
TLSConfig *tls.Config
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
// Handler executed when error happens in broker mesage
// processing
ErrorHandler Handler
TLSConfig *tls.Config
Addrs []string
Secure bool
}
type PublishOptions struct {
@@ -37,45 +25,28 @@ type PublishOptions struct {
}
type SubscribeOptions struct {
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
// AutoAck defaults to true. When a handler returns
// with a nil error the message is acked.
AutoAck bool
// Subscribers with the same queue name
// will create a shared subscription where each
// receives a subset of messages.
Queue string
// AutoAck defaults to true. When a handler returns
// with a nil error the message is acked.
AutoAck bool
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
}
type Option func(*Options)
type PublishOption func(*PublishOptions)
// PublishContext set context.
func PublishContext(ctx context.Context) PublishOption {
return func(o *PublishOptions) {
o.Context = ctx
}
}
type SubscribeOption func(*SubscribeOptions)
func NewOptions(opts ...Option) *Options {
options := Options{
Context: context.Background(),
Logger: logger.DefaultLogger,
}
for _, o := range opts {
o(&options)
}
return &options
}
var (
registryKey = "github.com/micro/go-micro/registry"
)
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
opt := SubscribeOptions{
@@ -89,7 +60,7 @@ func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
return opt
}
// Addrs sets the host addresses to be used by the broker.
// Addrs sets the host addresses to be used by the broker
func Addrs(addrs ...string) Option {
return func(o *Options) {
o.Addrs = addrs
@@ -97,7 +68,7 @@ func Addrs(addrs ...string) Option {
}
// Codec sets the codec used for encoding/decoding used where
// a broker does not support headers.
// a broker does not support headers
func Codec(c codec.Marshaler) Option {
return func(o *Options) {
o.Codec = c
@@ -112,15 +83,7 @@ func DisableAutoAck() SubscribeOption {
}
}
// ErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors.
func ErrorHandler(h Handler) Option {
return func(o *Options) {
o.ErrorHandler = h
}
}
// Queue sets the name of the queue to share messages on.
// Queue sets the name of the queue to share messages on
func Queue(name string) SubscribeOption {
return func(o *SubscribeOptions) {
o.Queue = name
@@ -129,32 +92,25 @@ func Queue(name string) SubscribeOption {
func Registry(r registry.Registry) Option {
return func(o *Options) {
o.Registry = r
o.Context = context.WithValue(o.Context, registryKey, r)
}
}
// Secure communication with the broker.
// Secure communication with the broker
func Secure(b bool) Option {
return func(o *Options) {
o.Secure = b
}
}
// Specify TLS Config.
// Specify TLS Config
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
// Logger sets the underline logger.
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// SubscribeContext set context.
// SubscribeContext set context
func SubscribeContext(ctx context.Context) SubscribeOption {
return func(o *SubscribeOptions) {
o.Context = ctx
-12
View File
@@ -1,12 +0,0 @@
package rabbitmq
type ExternalAuthentication struct {
}
func (auth *ExternalAuthentication) Mechanism() string {
return "EXTERNAL"
}
func (auth *ExternalAuthentication) Response() string {
return ""
}
-178
View File
@@ -1,178 +0,0 @@
package rabbitmq
//
// All credit to Mondo
//
import (
"errors"
"sync"
"github.com/google/uuid"
amqp "github.com/rabbitmq/amqp091-go"
)
type rabbitMQChannel struct {
uuid string
connection *amqp.Connection
channel *amqp.Channel
confirmPublish chan amqp.Confirmation
mtx sync.Mutex
}
func newRabbitChannel(conn *amqp.Connection, prefetchCount int, prefetchGlobal bool, confirmPublish bool) (*rabbitMQChannel, error) {
id, err := uuid.NewRandom()
if err != nil {
return nil, err
}
rabbitCh := &rabbitMQChannel{
uuid: id.String(),
connection: conn,
}
if err := rabbitCh.Connect(prefetchCount, prefetchGlobal, confirmPublish); err != nil {
return nil, err
}
return rabbitCh, nil
}
func (r *rabbitMQChannel) Connect(prefetchCount int, prefetchGlobal bool, confirmPublish bool) error {
var err error
r.channel, err = r.connection.Channel()
if err != nil {
return err
}
err = r.channel.Qos(prefetchCount, 0, prefetchGlobal)
if err != nil {
return err
}
if confirmPublish {
r.confirmPublish = r.channel.NotifyPublish(make(chan amqp.Confirmation, 1))
err = r.channel.Confirm(false)
if err != nil {
return err
}
}
return nil
}
func (r *rabbitMQChannel) Close() error {
if r.channel == nil {
return errors.New("Channel is nil")
}
return r.channel.Close()
}
func (r *rabbitMQChannel) Publish(exchange, key string, message amqp.Publishing) error {
if r.channel == nil {
return errors.New("Channel is nil")
}
if r.confirmPublish != nil {
r.mtx.Lock()
defer r.mtx.Unlock()
}
err := r.channel.Publish(exchange, key, false, false, message)
if err != nil {
return err
}
if r.confirmPublish != nil {
confirmation, ok := <-r.confirmPublish
if !ok {
return errors.New("Channel closed before could receive confirmation of publish")
}
if !confirmation.Ack {
return errors.New("Could not publish message, received nack from broker on confirmation")
}
}
return nil
}
func (r *rabbitMQChannel) DeclareExchange(ex Exchange) error {
return r.channel.ExchangeDeclare(
ex.Name, // name
string(ex.Type), // kind
ex.Durable, // durable
false, // autoDelete
false, // internal
false, // noWait
nil, // args
)
}
func (r *rabbitMQChannel) DeclareDurableExchange(ex Exchange) error {
return r.channel.ExchangeDeclare(
ex.Name, // name
string(ex.Type), // kind
true, // durable
false, // autoDelete
false, // internal
false, // noWait
nil, // args
)
}
func (r *rabbitMQChannel) DeclareQueue(queue string, args amqp.Table) error {
_, err := r.channel.QueueDeclare(
queue, // name
false, // durable
true, // autoDelete
false, // exclusive
false, // noWait
args, // args
)
return err
}
func (r *rabbitMQChannel) DeclareDurableQueue(queue string, args amqp.Table) error {
_, err := r.channel.QueueDeclare(
queue, // name
true, // durable
false, // autoDelete
false, // exclusive
false, // noWait
args, // args
)
return err
}
func (r *rabbitMQChannel) DeclareReplyQueue(queue string) error {
_, err := r.channel.QueueDeclare(
queue, // name
false, // durable
true, // autoDelete
true, // exclusive
false, // noWait
nil, // args
)
return err
}
func (r *rabbitMQChannel) ConsumeQueue(queue string, autoAck bool) (<-chan amqp.Delivery, error) {
return r.channel.Consume(
queue, // queue
r.uuid, // consumer
autoAck, // autoAck
false, // exclusive
false, // nolocal
false, // nowait
nil, // args
)
}
func (r *rabbitMQChannel) BindQueue(queue, key, exchange string, args amqp.Table) error {
return r.channel.QueueBind(
queue, // name
key, // key
exchange, // exchange
false, // noWait
args, // args
)
}
-299
View File
@@ -1,299 +0,0 @@
package rabbitmq
//
// All credit to Mondo
//
import (
"regexp"
"strings"
"sync"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"go-micro.dev/v5/logger"
mtls "go-micro.dev/v5/util/tls"
)
type MQExchangeType string
const (
ExchangeTypeFanout MQExchangeType = "fanout"
ExchangeTypeTopic = "topic"
ExchangeTypeDirect = "direct"
)
var (
DefaultExchange = Exchange{
Name: "micro",
Type: ExchangeTypeTopic,
}
DefaultRabbitURL = "amqp://guest:guest@127.0.0.1:5672"
DefaultPrefetchCount = 0
DefaultPrefetchGlobal = false
DefaultRequeueOnError = false
DefaultConfirmPublish = false
DefaultWithoutExchange = false
// The amqp library does not seem to set these when using amqp.DialConfig
// (even though it says so in the comments) so we set them manually to make
// sure to not brake any existing functionality.
defaultHeartbeat = 10 * time.Second
defaultLocale = "en_US"
defaultAmqpConfig = amqp.Config{
Heartbeat: defaultHeartbeat,
Locale: defaultLocale,
}
dial = amqp.Dial
dialTLS = amqp.DialTLS
dialConfig = amqp.DialConfig
)
type rabbitMQConn struct {
Connection *amqp.Connection
Channel *rabbitMQChannel
ExchangeChannel *rabbitMQChannel
exchange Exchange
withoutExchange bool
url string
prefetchCount int
prefetchGlobal bool
confirmPublish bool
sync.Mutex
connected bool
close chan bool
waitConnection chan struct{}
logger logger.Logger
}
// Exchange is the rabbitmq exchange.
type Exchange struct {
// Name of the exchange
Name string
// Type of the exchange
Type MQExchangeType
// Whether its persistent
Durable bool
}
func newRabbitMQConn(ex Exchange, urls []string, prefetchCount int, prefetchGlobal bool, confirmPublish bool, withoutExchange bool, logger logger.Logger) *rabbitMQConn {
var url string
if len(urls) > 0 && regexp.MustCompile("^amqp(s)?://.*").MatchString(urls[0]) {
url = urls[0]
} else {
url = DefaultRabbitURL
}
ret := &rabbitMQConn{
exchange: ex,
url: url,
withoutExchange: withoutExchange,
prefetchCount: prefetchCount,
prefetchGlobal: prefetchGlobal,
confirmPublish: confirmPublish,
close: make(chan bool),
waitConnection: make(chan struct{}),
logger: logger,
}
// its bad case of nil == waitConnection, so close it at start
close(ret.waitConnection)
return ret
}
func (r *rabbitMQConn) connect(secure bool, config *amqp.Config) error {
// try connect
if err := r.tryConnect(secure, config); err != nil {
return err
}
// connected
r.Lock()
r.connected = true
r.Unlock()
// create reconnect loop
go r.reconnect(secure, config)
return nil
}
func (r *rabbitMQConn) reconnect(secure bool, config *amqp.Config) {
// skip first connect
var connect bool
for {
if connect {
// try reconnect
if err := r.tryConnect(secure, config); err != nil {
time.Sleep(1 * time.Second)
continue
}
// connected
r.Lock()
r.connected = true
r.Unlock()
// unblock resubscribe cycle - close channel
//at this point channel is created and unclosed - close it without any additional checks
close(r.waitConnection)
}
connect = true
notifyClose := make(chan *amqp.Error)
r.Connection.NotifyClose(notifyClose)
chanNotifyClose := make(chan *amqp.Error)
var channel *amqp.Channel
if !r.withoutExchange {
channel = r.ExchangeChannel.channel
} else {
channel = r.Channel.channel
}
channel.NotifyClose(chanNotifyClose)
// To avoid deadlocks it is necessary to consume the messages from all channels.
for notifyClose != nil || chanNotifyClose != nil {
// block until closed
select {
case err := <-chanNotifyClose:
r.logger.Log(logger.ErrorLevel, err)
// block all resubscribe attempt - they are useless because there is no connection to rabbitmq
// create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks)
r.Lock()
r.connected = false
r.waitConnection = make(chan struct{})
r.Unlock()
chanNotifyClose = nil
case err := <-notifyClose:
r.logger.Log(logger.ErrorLevel, err)
// block all resubscribe attempt - they are useless because there is no connection to rabbitmq
// create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks)
r.Lock()
r.connected = false
r.waitConnection = make(chan struct{})
r.Unlock()
notifyClose = nil
case <-r.close:
return
}
}
}
}
func (r *rabbitMQConn) Connect(secure bool, config *amqp.Config) error {
r.Lock()
// already connected
if r.connected {
r.Unlock()
return nil
}
// check it was closed
select {
case <-r.close:
r.close = make(chan bool)
default:
// no op
// new conn
}
r.Unlock()
return r.connect(secure, config)
}
func (r *rabbitMQConn) Close() error {
r.Lock()
defer r.Unlock()
select {
case <-r.close:
return nil
default:
close(r.close)
r.connected = false
}
return r.Connection.Close()
}
func (r *rabbitMQConn) tryConnect(secure bool, config *amqp.Config) error {
var err error
if config == nil {
config = &defaultAmqpConfig
}
url := r.url
if secure || config.TLSClientConfig != nil || strings.HasPrefix(r.url, "amqps://") {
if config.TLSClientConfig == nil {
// Use environment-based config - secure by default
config.TLSClientConfig = mtls.Config()
}
url = strings.Replace(r.url, "amqp://", "amqps://", 1)
}
r.Connection, err = dialConfig(url, *config)
if err != nil {
return err
}
if r.Channel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish); err != nil {
return err
}
if !r.withoutExchange {
if r.exchange.Durable {
r.Channel.DeclareDurableExchange(r.exchange)
} else {
r.Channel.DeclareExchange(r.exchange)
}
r.ExchangeChannel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish)
}
return err
}
func (r *rabbitMQConn) Consume(queue, key string, headers amqp.Table, qArgs amqp.Table, autoAck, durableQueue bool) (*rabbitMQChannel, <-chan amqp.Delivery, error) {
consumerChannel, err := newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish)
if err != nil {
return nil, nil, err
}
if durableQueue {
err = consumerChannel.DeclareDurableQueue(queue, qArgs)
} else {
err = consumerChannel.DeclareQueue(queue, qArgs)
}
if err != nil {
return nil, nil, err
}
deliveries, err := consumerChannel.ConsumeQueue(queue, autoAck)
if err != nil {
return nil, nil, err
}
if !r.withoutExchange {
err = consumerChannel.BindQueue(queue, key, r.exchange.Name, headers)
if err != nil {
return nil, nil, err
}
}
return consumerChannel, deliveries, nil
}
func (r *rabbitMQConn) Publish(exchange, key string, msg amqp.Publishing) error {
if r.withoutExchange {
return r.Channel.Publish("", key, msg)
}
return r.ExchangeChannel.Publish(exchange, key, msg)
}

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