2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,3 @@ | |||||||
| # These are supported funding model platforms | # These are supported funding model platforms | ||||||
|  |  | ||||||
| issuehunt: micro/development | github: asim | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								.github/generate.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										15
									
								
								.github/generate.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | #!/bin/bash -e | ||||||
|  |  | ||||||
|  | find . -type f -name '*.pb.*.go' -o -name '*.pb.go' -a ! -name 'message.pb.go' -delete | ||||||
|  | PROTOS=$(find . -type f -name '*.proto' | grep -v proto/google/api) | ||||||
|  |  | ||||||
|  | mkdir -p proto/google/api | ||||||
|  | curl -s -o proto/google/api/annotations.proto -L https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto | ||||||
|  | curl -s -o proto/google/api/http.proto -L https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto | ||||||
|  |  | ||||||
|  | for PROTO in $PROTOS; do | ||||||
|  |   echo $PROTO | ||||||
|  |   protoc -I./proto -I. -I$(dirname $PROTO) --go_out=plugins=grpc,paths=source_relative:. --micro_out=paths=source_relative:. $PROTO | ||||||
|  | done | ||||||
|  |  | ||||||
|  | rm -r proto | ||||||
							
								
								
									
										21
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | name: Docker | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |     - master | ||||||
|  |     tags: | ||||||
|  |     - v2.* | ||||||
|  |     - v3.* | ||||||
|  | jobs: | ||||||
|  |   build: | ||||||
|  |      runs-on: ubuntu-latest | ||||||
|  |      steps: | ||||||
|  |        - uses: actions/checkout@v2 | ||||||
|  |          name: Check out repository | ||||||
|  |        - uses: elgohr/Publish-Docker-Github-Action@2.12 | ||||||
|  |          name: Build and Push Docker Image | ||||||
|  |          with: | ||||||
|  |            name: micro/go-micro | ||||||
|  |            username: ${{ secrets.DOCKER_USERNAME }} | ||||||
|  |            password: ${{ secrets.DOCKER_PASSWORD }} | ||||||
							
								
								
									
										28
									
								
								.github/workflows/pr.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/pr.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | name: PR Sanity Check | ||||||
|  | on: pull_request | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |  | ||||||
|  |   prtest: | ||||||
|  |     name: PR sanity check | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |  | ||||||
|  |     - name: Set up Go 1.13 | ||||||
|  |       uses: actions/setup-go@v1 | ||||||
|  |       with: | ||||||
|  |         go-version: 1.13 | ||||||
|  |       id: go | ||||||
|  |  | ||||||
|  |     - name: Check out code into the Go module directory | ||||||
|  |       uses: actions/checkout@v2 | ||||||
|  |  | ||||||
|  |     - name: Get dependencies | ||||||
|  |       run: | | ||||||
|  |         go get -v -t -d ./... | ||||||
|  |  | ||||||
|  |     - name: Run tests | ||||||
|  |       id: tests | ||||||
|  |       env: | ||||||
|  |         IN_TRAVIS_CI: yes | ||||||
|  |       run: go test -v ./... | ||||||
							
								
								
									
										51
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | name: Run tests | ||||||
|  | on: [push] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |  | ||||||
|  |   test: | ||||||
|  |     name: Test repo | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |  | ||||||
|  |     - name: Set up Go 1.13 | ||||||
|  |       uses: actions/setup-go@v1 | ||||||
|  |       with: | ||||||
|  |         go-version: 1.13 | ||||||
|  |       id: go | ||||||
|  |  | ||||||
|  |     - name: Check out code into the Go module directory | ||||||
|  |       uses: actions/checkout@v2 | ||||||
|  |  | ||||||
|  |     - name: Get dependencies | ||||||
|  |       run: | | ||||||
|  |         go get -v -t -d ./... | ||||||
|  |  | ||||||
|  |     - name: Run tests | ||||||
|  |       id: tests | ||||||
|  |       env: | ||||||
|  |         IN_TRAVIS_CI: yes | ||||||
|  |       run: go test -v ./... | ||||||
|  |    | ||||||
|  |     - name: Notify of test failure | ||||||
|  |       if: failure() | ||||||
|  |       uses: rtCamp/action-slack-notify@v2.0.0 | ||||||
|  |       env: | ||||||
|  |         SLACK_CHANNEL: build | ||||||
|  |         SLACK_COLOR: '#BF280A' | ||||||
|  |         SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png | ||||||
|  |         SLACK_TITLE: Tests Failed | ||||||
|  |         SLACK_USERNAME: GitHub Actions | ||||||
|  |         SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} | ||||||
|  |          | ||||||
|  |     - name: Notify of test success | ||||||
|  |       if: success() | ||||||
|  |       uses: rtCamp/action-slack-notify@v2.0.0 | ||||||
|  |       env: | ||||||
|  |         SLACK_CHANNEL: build | ||||||
|  |         SLACK_COLOR: '#1FAD2B' | ||||||
|  |         SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png | ||||||
|  |         SLACK_TITLE: Tests Passed | ||||||
|  |         SLACK_USERNAME: GitHub Actions | ||||||
|  |         SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,17 +0,0 @@ | |||||||
| language: go |  | ||||||
| go: |  | ||||||
| - 1.13.x |  | ||||||
| env: |  | ||||||
|   - GO111MODULE=on IN_TRAVIS_CI=yes |  | ||||||
| before_script: |  | ||||||
|   - go install github.com/golangci/golangci-lint/cmd/golangci-lint |  | ||||||
| script: |  | ||||||
|   - golangci-lint run || true |  | ||||||
|   - go test -v -race ./... || true |  | ||||||
|   - go test -v ./... |  | ||||||
| notifications: |  | ||||||
|   slack: |  | ||||||
|     secure: aEvhLbhujaGaKSrOokiG3//PaVHTIrc3fBpoRbCRqfZpyq6WREoapJJhF+tIpWWOwaC9GmChbD6aHo/jMUgwKXVyPSaNjiEL87YzUUpL8B2zslNp1rgfTg/LrzthOx3Q1TYwpaAl3to0fuHUVFX4yMeC2vuThq7WSXgMMxFCtbc= |  | ||||||
| cache: |  | ||||||
|   directories: |  | ||||||
|     - $GOPATH/pkg/mod |  | ||||||
| @@ -5,7 +5,7 @@ RUN mkdir /user && \ | |||||||
|     echo 'nobody:x:65534:' > /user/group |     echo 'nobody:x:65534:' > /user/group | ||||||
|  |  | ||||||
| ENV GO111MODULE=on | ENV GO111MODULE=on | ||||||
| RUN apk --no-cache add make git gcc libtool musl-dev ca-certificates && \ | RUN apk --no-cache add make git gcc libtool musl-dev ca-certificates dumb-init && \ | ||||||
|     rm -rf /var/cache/apk/* /tmp/* |     rm -rf /var/cache/apk/* /tmp/* | ||||||
|  |  | ||||||
| WORKDIR / | WORKDIR / | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,18 +1,18 @@ | |||||||
| # Go Micro [](https://opensource.org/licenses/Apache-2.0) [](https://godoc.org/github.com/micro/go-micro) [](https://travis-ci.org/micro/go-micro) [](https://goreportcard.com/report/github.com/micro/go-micro) | # Go Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/micro/go-micro?tab=doc) [](https://travis-ci.org/micro/go-micro) [](https://goreportcard.com/report/github.com/micro/go-micro) <a href="https://slack.micro.mu"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a> | ||||||
|  |  | ||||||
| Go Micro is a framework for microservice development. | Go Micro is a framework for distributed systems development. | ||||||
|  |  | ||||||
| ## Overview | ## Overview | ||||||
|  |  | ||||||
| Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.  | 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  | 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.  | but everything can be easily swapped out.  | ||||||
|  |  | ||||||
| <img src="https://micro.mu/docs/images/go-micro.svg" /> | <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). | 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](https://micro.mu/slack) community. | Follow us on [Twitter](https://twitter.com/microhq) or join the [Community](https://slack.micro.mu). | ||||||
|  |  | ||||||
| ## Features | ## Features | ||||||
|  |  | ||||||
| @@ -32,11 +32,10 @@ and server handle this by default. This includes protobuf and json by default. | |||||||
|  |  | ||||||
| - **Request/Response** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous  | - **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  | 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. | transport is [gRPC](https://grpc.io/). | ||||||
|  |  | ||||||
| - **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.  | - **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  | Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker. | ||||||
| is enabled. |  | ||||||
|  |  | ||||||
| - **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces  | - **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  | are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology. Find plugins in  | ||||||
| @@ -44,5 +43,15 @@ are pluggable and allows Go Micro to be runtime agnostic. You can plugin any und | |||||||
|  |  | ||||||
| ## Getting Started | ## Getting Started | ||||||
|  |  | ||||||
|  | To make use of Go Micro | ||||||
|  |  | ||||||
|  | ```golang | ||||||
|  | import "github.com/micro/go-micro/v2" | ||||||
|  | ``` | ||||||
|  |  | ||||||
| See the [docs](https://micro.mu/docs/framework.html) for detailed information on the architecture, installation and use of go-micro. | See the [docs](https://micro.mu/docs/framework.html) for detailed information on the architecture, installation and use of go-micro. | ||||||
|  |  | ||||||
|  | ## License | ||||||
|  |  | ||||||
|  | Go Micro is Apache 2.0 licensed. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| # Go Micro [](https://opensource.org/licenses/Apache-2.0) [](https://godoc.org/github.com/micro/go-micro) [](https://travis-ci.org/micro/go-micro) [](https://goreportcard.com/report/github.com/micro/go-micro) | # Go Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/micro/go-micro?tab=doc) [](https://travis-ci.org/micro/go-micro) [](https://goreportcard.com/report/github.com/micro/go-micro) | ||||||
|  |  | ||||||
| Go Micro是基于Golang的微服务开发框架。 | Go Micro是基于Golang的微服务开发框架。 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,8 +6,8 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/bwmarrin/discordgo" | 	"github.com/bwmarrin/discordgo" | ||||||
| 	"github.com/micro/go-micro/agent/input" | 	"github.com/micro/go-micro/v2/agent/input" | ||||||
| 	"github.com/micro/go-micro/util/log" | 	"github.com/micro/go-micro/v2/logger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type discordConn struct { | type discordConn struct { | ||||||
| @@ -74,7 +74,9 @@ func (dc *discordConn) Send(e *input.Event) error { | |||||||
| 	fields := strings.Split(e.To, ":") | 	fields := strings.Split(e.To, ":") | ||||||
| 	_, err := dc.master.session.ChannelMessageSend(fields[0], string(e.Data)) | 	_, err := dc.master.session.ChannelMessageSend(fields[0], string(e.Data)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Log("[bot][loop][send]", err) | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Error("[bot][loop][send]", err) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,8 +8,8 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/bwmarrin/discordgo" | 	"github.com/bwmarrin/discordgo" | ||||||
| 	"github.com/micro/cli" | 	"github.com/micro/cli/v2" | ||||||
| 	"github.com/micro/go-micro/agent/input" | 	"github.com/micro/go-micro/v2/agent/input" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| @@ -36,20 +36,20 @@ type discordInput struct { | |||||||
|  |  | ||||||
| func (d *discordInput) Flags() []cli.Flag { | func (d *discordInput) Flags() []cli.Flag { | ||||||
| 	return []cli.Flag{ | 	return []cli.Flag{ | ||||||
| 		cli.StringFlag{ | 		&cli.StringFlag{ | ||||||
| 			Name:    "discord_token", | 			Name:    "discord_token", | ||||||
| 			EnvVar: "MICRO_DISCORD_TOKEN", | 			EnvVars: []string{"MICRO_DISCORD_TOKEN"}, | ||||||
| 			Usage:   "Discord token (prefix with Bot if it's for bot account)", | 			Usage:   "Discord token (prefix with Bot if it's for bot account)", | ||||||
| 		}, | 		}, | ||||||
| 		cli.StringFlag{ | 		&cli.StringFlag{ | ||||||
| 			Name:    "discord_whitelist", | 			Name:    "discord_whitelist", | ||||||
| 			EnvVar: "MICRO_DISCORD_WHITELIST", | 			EnvVars: []string{"MICRO_DISCORD_WHITELIST"}, | ||||||
| 			Usage:   "Discord Whitelist (seperated by ,)", | 			Usage:   "Discord Whitelist (seperated by ,)", | ||||||
| 		}, | 		}, | ||||||
| 		cli.StringFlag{ | 		&cli.StringFlag{ | ||||||
| 			Name:    "discord_prefix", | 			Name:    "discord_prefix", | ||||||
| 			Usage:   "Discord Prefix", | 			Usage:   "Discord Prefix", | ||||||
| 			EnvVar: "MICRO_DISCORD_PREFIX", | 			EnvVars: []string{"MICRO_DISCORD_PREFIX"}, | ||||||
| 			Value:   "Micro ", | 			Value:   "Micro ", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| @@ -87,7 +87,7 @@ func (d *discordInput) Start() error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
| 	d.session, err = discordgo.New(d.token) | 	d.session, err = discordgo.New("Bot " + d.token) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| package input | package input | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/micro/cli" | 	"github.com/micro/cli/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type EventType string | type EventType string | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/agent/input" | 	"github.com/micro/go-micro/v2/agent/input" | ||||||
| 	"github.com/nlopes/slack" | 	"github.com/nlopes/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,8 +4,8 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/micro/cli" | 	"github.com/micro/cli/v2" | ||||||
| 	"github.com/micro/go-micro/agent/input" | 	"github.com/micro/go-micro/v2/agent/input" | ||||||
| 	"github.com/nlopes/slack" | 	"github.com/nlopes/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -26,15 +26,15 @@ func init() { | |||||||
|  |  | ||||||
| func (p *slackInput) Flags() []cli.Flag { | func (p *slackInput) Flags() []cli.Flag { | ||||||
| 	return []cli.Flag{ | 	return []cli.Flag{ | ||||||
| 		cli.BoolFlag{ | 		&cli.BoolFlag{ | ||||||
| 			Name:    "slack_debug", | 			Name:    "slack_debug", | ||||||
| 			Usage:   "Slack debug output", | 			Usage:   "Slack debug output", | ||||||
| 			EnvVar: "MICRO_SLACK_DEBUG", | 			EnvVars: []string{"MICRO_SLACK_DEBUG"}, | ||||||
| 		}, | 		}, | ||||||
| 		cli.StringFlag{ | 		&cli.StringFlag{ | ||||||
| 			Name:    "slack_token", | 			Name:    "slack_token", | ||||||
| 			Usage:   "Slack token", | 			Usage:   "Slack token", | ||||||
| 			EnvVar: "MICRO_SLACK_TOKEN", | 			EnvVars: []string{"MICRO_SLACK_TOKEN"}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,8 +6,8 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/forestgiant/sliceutil" | 	"github.com/forestgiant/sliceutil" | ||||||
| 	"github.com/micro/go-micro/agent/input" | 	"github.com/micro/go-micro/v2/agent/input" | ||||||
| 	"github.com/micro/go-micro/util/log" | 	"github.com/micro/go-micro/v2/logger" | ||||||
| 	tgbotapi "gopkg.in/telegram-bot-api.v4" | 	tgbotapi "gopkg.in/telegram-bot-api.v4" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -104,7 +104,9 @@ func (tc *telegramConn) Send(event *input.Event) error { | |||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		// probably it could be because of nested HTML tags -- telegram doesn't allow nested tags | 		// probably it could be because of nested HTML tags -- telegram doesn't allow nested tags | ||||||
| 		log.Log("[telegram][Send] error:", err) | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Error("[telegram][Send] error:", err) | ||||||
|  | 		} | ||||||
| 		msgConfig.Text = "This bot couldn't send the response (Internal error)" | 		msgConfig.Text = "This bot couldn't send the response (Internal error)" | ||||||
| 		tc.input.api.Send(msgConfig) | 		tc.input.api.Send(msgConfig) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -5,8 +5,8 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/micro/cli" | 	"github.com/micro/cli/v2" | ||||||
| 	"github.com/micro/go-micro/agent/input" | 	"github.com/micro/go-micro/v2/agent/input" | ||||||
| 	tgbotapi "gopkg.in/telegram-bot-api.v4" | 	tgbotapi "gopkg.in/telegram-bot-api.v4" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -34,19 +34,19 @@ func init() { | |||||||
|  |  | ||||||
| func (ti *telegramInput) Flags() []cli.Flag { | func (ti *telegramInput) Flags() []cli.Flag { | ||||||
| 	return []cli.Flag{ | 	return []cli.Flag{ | ||||||
| 		cli.BoolFlag{ | 		&cli.BoolFlag{ | ||||||
| 			Name:    "telegram_debug", | 			Name:    "telegram_debug", | ||||||
| 			EnvVar: "MICRO_TELEGRAM_DEBUG", | 			EnvVars: []string{"MICRO_TELEGRAM_DEBUG"}, | ||||||
| 			Usage:   "Telegram debug output", | 			Usage:   "Telegram debug output", | ||||||
| 		}, | 		}, | ||||||
| 		cli.StringFlag{ | 		&cli.StringFlag{ | ||||||
| 			Name:    "telegram_token", | 			Name:    "telegram_token", | ||||||
| 			EnvVar: "MICRO_TELEGRAM_TOKEN", | 			EnvVars: []string{"MICRO_TELEGRAM_TOKEN"}, | ||||||
| 			Usage:   "Telegram token", | 			Usage:   "Telegram token", | ||||||
| 		}, | 		}, | ||||||
| 		cli.StringFlag{ | 		&cli.StringFlag{ | ||||||
| 			Name:    "telegram_whitelist", | 			Name:    "telegram_whitelist", | ||||||
| 			EnvVar: "MICRO_TELEGRAM_WHITELIST", | 			EnvVars: []string{"MICRO_TELEGRAM_WHITELIST"}, | ||||||
| 			Usage:   "Telegram bot's users (comma-separated values)", | 			Usage:   "Telegram bot's users (comma-separated values)", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,11 +1,15 @@ | |||||||
| // Code generated by protoc-gen-go. DO NOT EDIT. | // Code generated by protoc-gen-go. DO NOT EDIT. | ||||||
| // source: github.com/micro/go-micro/agent/proto/bot.proto | // source: agent/proto/bot.proto | ||||||
|  |  | ||||||
| package go_micro_bot | package go_micro_bot | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	context "context" | ||||||
| 	fmt "fmt" | 	fmt "fmt" | ||||||
| 	proto "github.com/golang/protobuf/proto" | 	proto "github.com/golang/protobuf/proto" | ||||||
|  | 	grpc "google.golang.org/grpc" | ||||||
|  | 	codes "google.golang.org/grpc/codes" | ||||||
|  | 	status "google.golang.org/grpc/status" | ||||||
| 	math "math" | 	math "math" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -30,7 +34,7 @@ func (m *HelpRequest) Reset()         { *m = HelpRequest{} } | |||||||
| func (m *HelpRequest) String() string { return proto.CompactTextString(m) } | func (m *HelpRequest) String() string { return proto.CompactTextString(m) } | ||||||
| func (*HelpRequest) ProtoMessage()    {} | func (*HelpRequest) ProtoMessage()    {} | ||||||
| func (*HelpRequest) Descriptor() ([]byte, []int) { | func (*HelpRequest) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_018e8d5b14a89d12, []int{0} | 	return fileDescriptor_79b974b8c77805fa, []int{0} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *HelpRequest) XXX_Unmarshal(b []byte) error { | func (m *HelpRequest) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -63,7 +67,7 @@ func (m *HelpResponse) Reset()         { *m = HelpResponse{} } | |||||||
| func (m *HelpResponse) String() string { return proto.CompactTextString(m) } | func (m *HelpResponse) String() string { return proto.CompactTextString(m) } | ||||||
| func (*HelpResponse) ProtoMessage()    {} | func (*HelpResponse) ProtoMessage()    {} | ||||||
| func (*HelpResponse) Descriptor() ([]byte, []int) { | func (*HelpResponse) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_018e8d5b14a89d12, []int{1} | 	return fileDescriptor_79b974b8c77805fa, []int{1} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *HelpResponse) XXX_Unmarshal(b []byte) error { | func (m *HelpResponse) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -109,7 +113,7 @@ func (m *ExecRequest) Reset()         { *m = ExecRequest{} } | |||||||
| func (m *ExecRequest) String() string { return proto.CompactTextString(m) } | func (m *ExecRequest) String() string { return proto.CompactTextString(m) } | ||||||
| func (*ExecRequest) ProtoMessage()    {} | func (*ExecRequest) ProtoMessage()    {} | ||||||
| func (*ExecRequest) Descriptor() ([]byte, []int) { | func (*ExecRequest) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_018e8d5b14a89d12, []int{2} | 	return fileDescriptor_79b974b8c77805fa, []int{2} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *ExecRequest) XXX_Unmarshal(b []byte) error { | func (m *ExecRequest) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -149,7 +153,7 @@ func (m *ExecResponse) Reset()         { *m = ExecResponse{} } | |||||||
| func (m *ExecResponse) String() string { return proto.CompactTextString(m) } | func (m *ExecResponse) String() string { return proto.CompactTextString(m) } | ||||||
| func (*ExecResponse) ProtoMessage()    {} | func (*ExecResponse) ProtoMessage()    {} | ||||||
| func (*ExecResponse) Descriptor() ([]byte, []int) { | func (*ExecResponse) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_018e8d5b14a89d12, []int{3} | 	return fileDescriptor_79b974b8c77805fa, []int{3} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *ExecResponse) XXX_Unmarshal(b []byte) error { | func (m *ExecResponse) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -191,26 +195,139 @@ func init() { | |||||||
| 	proto.RegisterType((*ExecResponse)(nil), "go.micro.bot.ExecResponse") | 	proto.RegisterType((*ExecResponse)(nil), "go.micro.bot.ExecResponse") | ||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func init() { proto.RegisterFile("agent/proto/bot.proto", fileDescriptor_79b974b8c77805fa) } | ||||||
| 	proto.RegisterFile("github.com/micro/go-micro/agent/proto/bot.proto", fileDescriptor_018e8d5b14a89d12) |  | ||||||
|  | var fileDescriptor_79b974b8c77805fa = []byte{ | ||||||
|  | 	// 234 bytes of a gzipped FileDescriptorProto | ||||||
|  | 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x3f, 0x4f, 0xc3, 0x30, | ||||||
|  | 	0x10, 0xc5, 0x1b, 0x28, 0x45, 0xbd, 0x84, 0xc5, 0x02, 0x14, 0x3a, 0x05, 0x4f, 0x9d, 0x5c, 0x09, | ||||||
|  | 	0x56, 0x24, 0x06, 0x04, 0x62, 0xce, 0x37, 0x48, 0xd2, 0x53, 0x14, 0xa9, 0xf1, 0x99, 0xb3, 0x23, | ||||||
|  | 	0xf1, 0x1d, 0xf8, 0xd2, 0xc8, 0x7f, 0x06, 0xab, 0xea, 0x76, 0xcf, 0x67, 0xbd, 0xf7, 0x7b, 0x07, | ||||||
|  | 	0x0f, 0xdd, 0x88, 0xda, 0x1d, 0x0c, 0x93, 0xa3, 0x43, 0x4f, 0x4e, 0x85, 0x49, 0x54, 0x23, 0xa9, | ||||||
|  | 	0x79, 0x1a, 0x98, 0x54, 0x4f, 0x4e, 0xde, 0x41, 0xf9, 0x8d, 0x27, 0xd3, 0xe2, 0xcf, 0x82, 0xd6, | ||||||
|  | 	0xc9, 0x2f, 0xa8, 0xa2, 0xb4, 0x86, 0xb4, 0x45, 0x71, 0x0f, 0x37, 0x8b, 0xed, 0x46, 0xac, 0x8b, | ||||||
|  | 	0xa6, 0xd8, 0x6f, 0xdb, 0x28, 0x44, 0x03, 0xe5, 0x11, 0xed, 0xc0, 0x93, 0x71, 0x13, 0xe9, 0xfa, | ||||||
|  | 	0x2a, 0xec, 0xf2, 0x27, 0xf9, 0x0c, 0xe5, 0xe7, 0x2f, 0x0e, 0xc9, 0x56, 0x08, 0x58, 0x77, 0x3c, | ||||||
|  | 	0xda, 0xba, 0x68, 0xae, 0xf7, 0xdb, 0x36, 0xcc, 0xf2, 0x0d, 0xaa, 0xf8, 0x25, 0x45, 0x3d, 0xc2, | ||||||
|  | 	0x86, 0xd1, 0x2e, 0x27, 0x17, 0xb2, 0xaa, 0x36, 0x29, 0x8f, 0x80, 0xcc, 0xc4, 0x29, 0x26, 0x8a, | ||||||
|  | 	0x97, 0xbf, 0x02, 0x6e, 0x3f, 0x68, 0x9e, 0x3b, 0x7d, 0x14, 0xef, 0xb0, 0xf6, 0xd0, 0xe2, 0x49, | ||||||
|  | 	0xe5, 0xd5, 0x54, 0xd6, 0x6b, 0xb7, 0xbb, 0xb4, 0x8a, 0xc1, 0x72, 0xe5, 0x0d, 0x3c, 0xca, 0xb9, | ||||||
|  | 	0x41, 0xd6, 0xe0, 0xdc, 0x20, 0x27, 0x97, 0xab, 0x7e, 0x13, 0x4e, 0xfb, 0xfa, 0x1f, 0x00, 0x00, | ||||||
|  | 	0xff, 0xff, 0xe8, 0x08, 0x5e, 0xad, 0x73, 0x01, 0x00, 0x00, | ||||||
| } | } | ||||||
|  |  | ||||||
| var fileDescriptor_018e8d5b14a89d12 = []byte{ | // Reference imports to suppress errors if they are not otherwise used. | ||||||
| 	// 246 bytes of a gzipped FileDescriptorProto | var _ context.Context | ||||||
| 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x50, 0x4d, 0x4b, 0xc4, 0x30, | var _ grpc.ClientConn | ||||||
| 	0x10, 0xdd, 0xea, 0xba, 0xb2, 0xd3, 0x7a, 0x09, 0x22, 0x75, 0x4f, 0x35, 0xa7, 0xbd, 0x98, 0x80, |  | ||||||
| 	0x5e, 0x05, 0x0f, 0xa2, 0x78, 0xee, 0x3f, 0x68, 0xbb, 0x43, 0x2c, 0x6c, 0x3b, 0x35, 0x99, 0x82, | // This is a compile-time assertion to ensure that this generated file | ||||||
| 	0xff, 0xc1, 0x3f, 0x2d, 0x4d, 0x72, 0x08, 0xc5, 0xdb, 0x7b, 0x79, 0xe1, 0x7d, 0x0c, 0x68, 0xd3, | // is compatible with the grpc package it is being compiled against. | ||||||
| 	0xf3, 0xd7, 0xdc, 0xaa, 0x8e, 0x06, 0x3d, 0xf4, 0x9d, 0x25, 0x6d, 0xe8, 0x31, 0x80, 0xc6, 0xe0, | const _ = grpc.SupportPackageIsVersion4 | ||||||
| 	0xc8, 0x7a, 0xb2, 0xc4, 0xa4, 0x5b, 0x62, 0xe5, 0x91, 0x28, 0x0c, 0x29, 0xaf, 0xab, 0x96, 0x58, |  | ||||||
| 	0xde, 0x40, 0xfe, 0x89, 0xe7, 0xa9, 0xc6, 0xef, 0x19, 0x1d, 0xcb, 0x0f, 0x28, 0x02, 0x75, 0x13, | // CommandClient is the client API for Command service. | ||||||
| 	0x8d, 0x0e, 0xc5, 0x2d, 0x5c, 0xcd, 0xae, 0x31, 0x58, 0x66, 0x55, 0x76, 0xdc, 0xd7, 0x81, 0x88, | // | ||||||
| 	0x0a, 0xf2, 0x13, 0xba, 0xce, 0xf6, 0x13, 0xf7, 0x34, 0x96, 0x17, 0x5e, 0x4b, 0x9f, 0xe4, 0x03, | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. | ||||||
| 	0xe4, 0xef, 0x3f, 0xd8, 0x45, 0x5b, 0x21, 0x60, 0xdb, 0x58, 0xe3, 0xca, 0xac, 0xba, 0x3c, 0xee, | type CommandClient interface { | ||||||
| 	0x6b, 0x8f, 0xe5, 0x0b, 0x14, 0xe1, 0x4b, 0x8c, 0xba, 0x83, 0x9d, 0x45, 0x37, 0x9f, 0xd9, 0x67, | 	Help(ctx context.Context, in *HelpRequest, opts ...grpc.CallOption) (*HelpResponse, error) | ||||||
| 	0x15, 0x75, 0x64, 0x4b, 0x05, 0xb4, 0x96, 0x6c, 0x8c, 0x09, 0xe4, 0xe9, 0x37, 0x83, 0xeb, 0x37, | 	Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) | ||||||
| 	0x1a, 0x86, 0x66, 0x3c, 0x89, 0x57, 0xd8, 0x2e, 0xa5, 0xc5, 0xbd, 0x4a, 0xa7, 0xa9, 0x64, 0xd7, | } | ||||||
| 	0xe1, 0xf0, 0x9f, 0x14, 0x82, 0xe5, 0x66, 0x31, 0x58, 0xaa, 0xac, 0x0d, 0x92, 0x05, 0x6b, 0x83, |  | ||||||
| 	0xb4, 0xb9, 0xdc, 0xb4, 0x3b, 0x7f, 0xda, 0xe7, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x18, 0xbd, | type commandClient struct { | ||||||
| 	0x39, 0x29, 0x8d, 0x01, 0x00, 0x00, | 	cc *grpc.ClientConn | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewCommandClient(cc *grpc.ClientConn) CommandClient { | ||||||
|  | 	return &commandClient{cc} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *commandClient) Help(ctx context.Context, in *HelpRequest, opts ...grpc.CallOption) (*HelpResponse, error) { | ||||||
|  | 	out := new(HelpResponse) | ||||||
|  | 	err := c.cc.Invoke(ctx, "/go.micro.bot.Command/Help", in, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *commandClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) { | ||||||
|  | 	out := new(ExecResponse) | ||||||
|  | 	err := c.cc.Invoke(ctx, "/go.micro.bot.Command/Exec", in, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CommandServer is the server API for Command service. | ||||||
|  | type CommandServer interface { | ||||||
|  | 	Help(context.Context, *HelpRequest) (*HelpResponse, error) | ||||||
|  | 	Exec(context.Context, *ExecRequest) (*ExecResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnimplementedCommandServer can be embedded to have forward compatible implementations. | ||||||
|  | type UnimplementedCommandServer struct { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (*UnimplementedCommandServer) Help(ctx context.Context, req *HelpRequest) (*HelpResponse, error) { | ||||||
|  | 	return nil, status.Errorf(codes.Unimplemented, "method Help not implemented") | ||||||
|  | } | ||||||
|  | func (*UnimplementedCommandServer) Exec(ctx context.Context, req *ExecRequest) (*ExecResponse, error) { | ||||||
|  | 	return nil, status.Errorf(codes.Unimplemented, "method Exec not implemented") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterCommandServer(s *grpc.Server, srv CommandServer) { | ||||||
|  | 	s.RegisterService(&_Command_serviceDesc, srv) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func _Command_Help_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | ||||||
|  | 	in := new(HelpRequest) | ||||||
|  | 	if err := dec(in); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if interceptor == nil { | ||||||
|  | 		return srv.(CommandServer).Help(ctx, in) | ||||||
|  | 	} | ||||||
|  | 	info := &grpc.UnaryServerInfo{ | ||||||
|  | 		Server:     srv, | ||||||
|  | 		FullMethod: "/go.micro.bot.Command/Help", | ||||||
|  | 	} | ||||||
|  | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) { | ||||||
|  | 		return srv.(CommandServer).Help(ctx, req.(*HelpRequest)) | ||||||
|  | 	} | ||||||
|  | 	return interceptor(ctx, in, info, handler) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func _Command_Exec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | ||||||
|  | 	in := new(ExecRequest) | ||||||
|  | 	if err := dec(in); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if interceptor == nil { | ||||||
|  | 		return srv.(CommandServer).Exec(ctx, in) | ||||||
|  | 	} | ||||||
|  | 	info := &grpc.UnaryServerInfo{ | ||||||
|  | 		Server:     srv, | ||||||
|  | 		FullMethod: "/go.micro.bot.Command/Exec", | ||||||
|  | 	} | ||||||
|  | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) { | ||||||
|  | 		return srv.(CommandServer).Exec(ctx, req.(*ExecRequest)) | ||||||
|  | 	} | ||||||
|  | 	return interceptor(ctx, in, info, handler) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _Command_serviceDesc = grpc.ServiceDesc{ | ||||||
|  | 	ServiceName: "go.micro.bot.Command", | ||||||
|  | 	HandlerType: (*CommandServer)(nil), | ||||||
|  | 	Methods: []grpc.MethodDesc{ | ||||||
|  | 		{ | ||||||
|  | 			MethodName: "Help", | ||||||
|  | 			Handler:    _Command_Help_Handler, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			MethodName: "Exec", | ||||||
|  | 			Handler:    _Command_Exec_Handler, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	Streams:  []grpc.StreamDesc{}, | ||||||
|  | 	Metadata: "agent/proto/bot.proto", | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| // Code generated by protoc-gen-micro. DO NOT EDIT. | // Code generated by protoc-gen-micro. DO NOT EDIT. | ||||||
| // source: github.com/micro/go-micro/agent/proto/bot.proto | // source: agent/proto/bot.proto | ||||||
|  |  | ||||||
| package go_micro_bot | package go_micro_bot | ||||||
|  |  | ||||||
| @@ -11,8 +11,9 @@ import ( | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	context "context" | 	context "context" | ||||||
| 	client "github.com/micro/go-micro/client" | 	api "github.com/micro/go-micro/v2/api" | ||||||
| 	server "github.com/micro/go-micro/server" | 	client "github.com/micro/go-micro/v2/client" | ||||||
|  | 	server "github.com/micro/go-micro/v2/server" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Reference imports to suppress errors if they are not otherwise used. | // Reference imports to suppress errors if they are not otherwise used. | ||||||
| @@ -27,10 +28,17 @@ var _ = math.Inf | |||||||
| const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package | ||||||
|  |  | ||||||
| // Reference imports to suppress errors if they are not otherwise used. | // Reference imports to suppress errors if they are not otherwise used. | ||||||
|  | var _ api.Endpoint | ||||||
| var _ context.Context | var _ context.Context | ||||||
| var _ client.Option | var _ client.Option | ||||||
| var _ server.Option | var _ server.Option | ||||||
|  |  | ||||||
|  | // Api Endpoints for Command service | ||||||
|  |  | ||||||
|  | func NewCommandEndpoints() []*api.Endpoint { | ||||||
|  | 	return []*api.Endpoint{} | ||||||
|  | } | ||||||
|  |  | ||||||
| // Client API for Command service | // Client API for Command service | ||||||
|  |  | ||||||
| type CommandService interface { | type CommandService interface { | ||||||
| @@ -44,12 +52,6 @@ type commandService struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func NewCommandService(name string, c client.Client) CommandService { | func NewCommandService(name string, c client.Client) CommandService { | ||||||
| 	if c == nil { |  | ||||||
| 		c = client.NewClient() |  | ||||||
| 	} |  | ||||||
| 	if len(name) == 0 { |  | ||||||
| 		name = "go.micro.bot" |  | ||||||
| 	} |  | ||||||
| 	return &commandService{ | 	return &commandService{ | ||||||
| 		c:    c, | 		c:    c, | ||||||
| 		name: name, | 		name: name, | ||||||
|   | |||||||
							
								
								
									
										61
									
								
								api/api.go
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								api/api.go
									
									
									
									
									
								
							| @@ -5,10 +5,27 @@ import ( | |||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/registry" | 	"github.com/micro/go-micro/v2/registry" | ||||||
| 	"github.com/micro/go-micro/server" | 	"github.com/micro/go-micro/v2/server" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | type Api interface { | ||||||
|  | 	// Initialise options | ||||||
|  | 	Init(...Option) error | ||||||
|  | 	// Get the options | ||||||
|  | 	Options() Options | ||||||
|  | 	// Register a http handler | ||||||
|  | 	Register(*Endpoint) error | ||||||
|  | 	// Register a route | ||||||
|  | 	Deregister(*Endpoint) error | ||||||
|  | 	// Implemenation of api | ||||||
|  | 	String() string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Options struct{} | ||||||
|  |  | ||||||
|  | type Option func(*Options) error | ||||||
|  |  | ||||||
| // Endpoint is a mapping between an RPC method and HTTP endpoint | // Endpoint is a mapping between an RPC method and HTTP endpoint | ||||||
| type Endpoint struct { | type Endpoint struct { | ||||||
| 	// RPC Method e.g. Greeter.Hello | 	// RPC Method e.g. Greeter.Hello | ||||||
| @@ -23,6 +40,12 @@ type Endpoint struct { | |||||||
| 	Method []string | 	Method []string | ||||||
| 	// HTTP Path e.g /greeter. Expect POSIX regex | 	// HTTP Path e.g /greeter. Expect POSIX regex | ||||||
| 	Path []string | 	Path []string | ||||||
|  | 	// Body destination | ||||||
|  | 	// "*" or "" - top level message value | ||||||
|  | 	// "string" - inner message value | ||||||
|  | 	Body string | ||||||
|  | 	// Stream flag | ||||||
|  | 	Stream bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // Service represents an API service | // Service represents an API service | ||||||
| @@ -57,14 +80,25 @@ func Encode(e *Endpoint) map[string]string { | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return map[string]string{ | 	// endpoint map | ||||||
| 		"endpoint":    e.Name, | 	ep := make(map[string]string) | ||||||
| 		"description": e.Description, |  | ||||||
| 		"method":      strings.Join(e.Method, ","), | 	// set vals only if they exist | ||||||
| 		"path":        strings.Join(e.Path, ","), | 	set := func(k, v string) { | ||||||
| 		"host":        strings.Join(e.Host, ","), | 		if len(v) == 0 { | ||||||
| 		"handler":     e.Handler, | 			return | ||||||
| 		} | 		} | ||||||
|  | 		ep[k] = v | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	set("endpoint", e.Name) | ||||||
|  | 	set("description", e.Description) | ||||||
|  | 	set("handler", e.Handler) | ||||||
|  | 	set("method", strings.Join(e.Method, ",")) | ||||||
|  | 	set("path", strings.Join(e.Path, ",")) | ||||||
|  | 	set("host", strings.Join(e.Host, ",")) | ||||||
|  |  | ||||||
|  | 	return ep | ||||||
| } | } | ||||||
|  |  | ||||||
| // Decode decodes endpoint metadata into an endpoint | // Decode decodes endpoint metadata into an endpoint | ||||||
| @@ -94,10 +128,19 @@ func Validate(e *Endpoint) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, p := range e.Path { | 	for _, p := range e.Path { | ||||||
|  | 		ps := p[0] | ||||||
|  | 		pe := p[len(p)-1] | ||||||
|  |  | ||||||
|  | 		if ps == '^' && pe == '$' { | ||||||
| 			_, err := regexp.CompilePOSIX(p) | 			_, err := regexp.CompilePOSIX(p) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
|  | 		} else if ps == '^' && pe != '$' { | ||||||
|  | 			return errors.New("invalid path") | ||||||
|  | 		} else if ps != '^' && pe == '$' { | ||||||
|  | 			return errors.New("invalid path") | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(e.Handler) == 0 { | 	if len(e.Handler) == 0 { | ||||||
|   | |||||||
| @@ -111,3 +111,42 @@ func TestEncoding(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestValidate(t *testing.T) { | ||||||
|  | 	epPcre := &Endpoint{ | ||||||
|  | 		Name:        "Foo.Bar", | ||||||
|  | 		Description: "A test endpoint", | ||||||
|  | 		Handler:     "meta", | ||||||
|  | 		Host:        []string{"foo.com"}, | ||||||
|  | 		Method:      []string{"GET"}, | ||||||
|  | 		Path:        []string{"^/test/?$"}, | ||||||
|  | 	} | ||||||
|  | 	if err := Validate(epPcre); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	epGpath := &Endpoint{ | ||||||
|  | 		Name:        "Foo.Bar", | ||||||
|  | 		Description: "A test endpoint", | ||||||
|  | 		Handler:     "meta", | ||||||
|  | 		Host:        []string{"foo.com"}, | ||||||
|  | 		Method:      []string{"GET"}, | ||||||
|  | 		Path:        []string{"/test/{id}"}, | ||||||
|  | 	} | ||||||
|  | 	if err := Validate(epGpath); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	epPcreInvalid := &Endpoint{ | ||||||
|  | 		Name:        "Foo.Bar", | ||||||
|  | 		Description: "A test endpoint", | ||||||
|  | 		Handler:     "meta", | ||||||
|  | 		Host:        []string{"foo.com"}, | ||||||
|  | 		Method:      []string{"GET"}, | ||||||
|  | 		Path:        []string{"/test/?$"}, | ||||||
|  | 	} | ||||||
|  | 	if err := Validate(epPcreInvalid); err == nil { | ||||||
|  | 		t.Fatalf("invalid pcre %v", epPcreInvalid.Path[0]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,13 +4,13 @@ package api | |||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  |  | ||||||
| 	goapi "github.com/micro/go-micro/api" | 	goapi "github.com/micro/go-micro/v2/api" | ||||||
| 	"github.com/micro/go-micro/api/handler" | 	"github.com/micro/go-micro/v2/api/handler" | ||||||
| 	api "github.com/micro/go-micro/api/proto" | 	api "github.com/micro/go-micro/v2/api/proto" | ||||||
| 	"github.com/micro/go-micro/client" | 	"github.com/micro/go-micro/v2/client" | ||||||
| 	"github.com/micro/go-micro/client/selector" | 	"github.com/micro/go-micro/v2/client/selector" | ||||||
| 	"github.com/micro/go-micro/errors" | 	"github.com/micro/go-micro/v2/errors" | ||||||
| 	"github.com/micro/go-micro/util/ctx" | 	"github.com/micro/go-micro/v2/util/ctx" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type apiHandler struct { | type apiHandler struct { | ||||||
| @@ -24,6 +24,12 @@ const ( | |||||||
|  |  | ||||||
| // API handler is the default handler which takes api.Request and returns api.Response | // API handler is the default handler which takes api.Request and returns api.Response | ||||||
| func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	bsize := handler.DefaultMaxRecvSize | ||||||
|  | 	if a.opts.MaxRecvSize > 0 { | ||||||
|  | 		bsize = a.opts.MaxRecvSize | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.Body = http.MaxBytesReader(w, r.Body, bsize) | ||||||
| 	request, err := requestToProto(r) | 	request, err := requestToProto(r) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		er := errors.InternalServerError("go.micro.api", err.Error()) | 		er := errors.InternalServerError("go.micro.api", err.Error()) | ||||||
| @@ -59,7 +65,7 @@ func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// create request and response | 	// create request and response | ||||||
| 	c := a.opts.Service.Client() | 	c := a.opts.Client | ||||||
| 	req := c.NewRequest(service.Name, service.Endpoint.Name, request) | 	req := c.NewRequest(service.Name, service.Endpoint.Name, request) | ||||||
| 	rsp := &api.Response{} | 	rsp := &api.Response{} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,15 +2,20 @@ package api | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"mime" | 	"mime" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	api "github.com/micro/go-micro/api/proto" | 	api "github.com/micro/go-micro/v2/api/proto" | ||||||
| 	"github.com/micro/go-micro/client/selector" | 	"github.com/micro/go-micro/v2/client/selector" | ||||||
| 	"github.com/micro/go-micro/registry" | 	"github.com/micro/go-micro/v2/registry" | ||||||
|  | 	"github.com/oxtoacart/bpool" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// need to calculate later to specify useful defaults | ||||||
|  | 	bufferPool = bpool.NewSizedBufferPool(1024, 8) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func requestToProto(r *http.Request) (*api.Request, error) { | func requestToProto(r *http.Request) (*api.Request, error) { | ||||||
| @@ -39,9 +44,12 @@ func requestToProto(r *http.Request) (*api.Request, error) { | |||||||
| 		case "application/x-www-form-urlencoded": | 		case "application/x-www-form-urlencoded": | ||||||
| 			// expect form vals in Post data | 			// expect form vals in Post data | ||||||
| 		default: | 		default: | ||||||
|  | 			buf := bufferPool.Get() | ||||||
| 			data, _ := ioutil.ReadAll(r.Body) | 			defer bufferPool.Put(buf) | ||||||
| 			req.Body = string(data) | 			if _, err = buf.ReadFrom(r.Body); err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			req.Body = buf.String() | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,268 +0,0 @@ | |||||||
| // 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...), |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,94 +0,0 @@ | |||||||
| // 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...), |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,282 +0,0 @@ | |||||||
| /* |  | ||||||
|  * 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" |  | ||||||
| 	validator "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 |  | ||||||
| } |  | ||||||
| @@ -4,7 +4,6 @@ package event | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path" | 	"path" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| @@ -12,13 +11,18 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"github.com/micro/go-micro/api/handler" | 	"github.com/micro/go-micro/v2/api/handler" | ||||||
| 	proto "github.com/micro/go-micro/api/proto" | 	proto "github.com/micro/go-micro/v2/api/proto" | ||||||
| 	"github.com/micro/go-micro/util/ctx" | 	"github.com/micro/go-micro/v2/util/ctx" | ||||||
|  | 	"github.com/oxtoacart/bpool" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	bufferPool = bpool.NewSizedBufferPool(1024, 8) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type event struct { | type event struct { | ||||||
| 	options handler.Options | 	opts handler.Options | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| @@ -64,11 +68,18 @@ func evRoute(ns, p string) (string, string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) { | func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	bsize := handler.DefaultMaxRecvSize | ||||||
|  | 	if e.opts.MaxRecvSize > 0 { | ||||||
|  | 		bsize = e.opts.MaxRecvSize | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.Body = http.MaxBytesReader(w, r.Body, bsize) | ||||||
|  |  | ||||||
| 	// request to topic:event | 	// request to topic:event | ||||||
| 	// create event | 	// create event | ||||||
| 	// publish to topic | 	// publish to topic | ||||||
|  |  | ||||||
| 	topic, action := evRoute(e.options.Namespace, r.URL.Path) | 	topic, action := evRoute(e.opts.Namespace, r.URL.Path) | ||||||
|  |  | ||||||
| 	// create event | 	// create event | ||||||
| 	ev := &proto.Event{ | 	ev := &proto.Event{ | ||||||
| @@ -96,16 +107,18 @@ func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 		bytes, _ := json.Marshal(r.URL.Query()) | 		bytes, _ := json.Marshal(r.URL.Query()) | ||||||
| 		ev.Data = string(bytes) | 		ev.Data = string(bytes) | ||||||
| 	} else { | 	} else { | ||||||
| 		b, err := ioutil.ReadAll(r.Body) | 		// Read body | ||||||
| 		if err != nil { | 		buf := bufferPool.Get() | ||||||
|  | 		defer bufferPool.Put(buf) | ||||||
|  | 		if _, err := buf.ReadFrom(r.Body); err != nil { | ||||||
| 			http.Error(w, err.Error(), 500) | 			http.Error(w, err.Error(), 500) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		ev.Data = string(b) | 		ev.Data = buf.String() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// get client | 	// get client | ||||||
| 	c := e.options.Service.Client() | 	c := e.opts.Client | ||||||
|  |  | ||||||
| 	// create publication | 	// create publication | ||||||
| 	p := c.NewMessage(topic, ev) | 	p := c.NewMessage(topic, ev) | ||||||
| @@ -123,6 +136,6 @@ func (e *event) String() string { | |||||||
|  |  | ||||||
| func NewHandler(opts ...handler.Option) handler.Handler { | func NewHandler(opts ...handler.Option) handler.Handler { | ||||||
| 	return &event{ | 	return &event{ | ||||||
| 		options: handler.NewOptions(opts...), | 		opts: handler.NewOptions(opts...), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +0,0 @@ | |||||||
| // 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" |  | ||||||
| } |  | ||||||
| @@ -8,9 +8,9 @@ import ( | |||||||
| 	"net/http/httputil" | 	"net/http/httputil" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api" | 	"github.com/micro/go-micro/v2/api" | ||||||
| 	"github.com/micro/go-micro/api/handler" | 	"github.com/micro/go-micro/v2/api/handler" | ||||||
| 	"github.com/micro/go-micro/client/selector" | 	"github.com/micro/go-micro/v2/client/selector" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
|   | |||||||
| @@ -6,17 +6,17 @@ import ( | |||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api/handler" | 	"github.com/micro/go-micro/v2/api/handler" | ||||||
| 	"github.com/micro/go-micro/api/router" | 	"github.com/micro/go-micro/v2/api/resolver" | ||||||
| 	regRouter "github.com/micro/go-micro/api/router/registry" | 	"github.com/micro/go-micro/v2/api/resolver/vpath" | ||||||
| 	"github.com/micro/go-micro/config/cmd" | 	"github.com/micro/go-micro/v2/api/router" | ||||||
| 	"github.com/micro/go-micro/registry" | 	regRouter "github.com/micro/go-micro/v2/api/router/registry" | ||||||
| 	"github.com/micro/go-micro/registry/memory" | 	"github.com/micro/go-micro/v2/registry" | ||||||
|  | 	"github.com/micro/go-micro/v2/registry/memory" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func testHttp(t *testing.T, path, service, ns string) { | func testHttp(t *testing.T, path, service, ns string) { | ||||||
| 	r := memory.NewRegistry() | 	r := memory.NewRegistry() | ||||||
| 	cmd.DefaultCmd = cmd.NewCmd(cmd.Registry(&r)) |  | ||||||
|  |  | ||||||
| 	l, err := net.Listen("tcp", "127.0.0.1:0") | 	l, err := net.Listen("tcp", "127.0.0.1:0") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -56,7 +56,10 @@ func testHttp(t *testing.T, path, service, ns string) { | |||||||
| 	// initialise the handler | 	// initialise the handler | ||||||
| 	rt := regRouter.NewRouter( | 	rt := regRouter.NewRouter( | ||||||
| 		router.WithHandler("http"), | 		router.WithHandler("http"), | ||||||
| 		router.WithNamespace(ns), | 		router.WithRegistry(r), | ||||||
|  | 		router.WithResolver(vpath.NewResolver( | ||||||
|  | 			resolver.WithNamespace(resolver.StaticNamespace(ns)), | ||||||
|  | 		)), | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
| 	p := NewHandler(handler.WithRouter(rt)) | 	p := NewHandler(handler.WithRouter(rt)) | ||||||
| @@ -117,6 +120,8 @@ func TestHttpHandler(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, d := range testData { | 	for _, d := range testData { | ||||||
|  | 		t.Run(d.service, func(t *testing.T) { | ||||||
| 			testHttp(t, d.path, d.service, d.namespace) | 			testHttp(t, d.path, d.service, d.namespace) | ||||||
|  | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,20 @@ | |||||||
| package handler | package handler | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/micro/go-micro" | 	"github.com/micro/go-micro/v2/api/router" | ||||||
| 	"github.com/micro/go-micro/api/router" | 	"github.com/micro/go-micro/v2/client" | ||||||
|  | 	"github.com/micro/go-micro/v2/client/grpc" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	DefaultMaxRecvSize int64 = 1024 * 1024 * 100 // 10Mb | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Options struct { | type Options struct { | ||||||
|  | 	MaxRecvSize int64 | ||||||
| 	Namespace   string | 	Namespace   string | ||||||
| 	Router      router.Router | 	Router      router.Router | ||||||
| 	Service   micro.Service | 	Client      client.Client | ||||||
| } | } | ||||||
|  |  | ||||||
| type Option func(o *Options) | type Option func(o *Options) | ||||||
| @@ -20,9 +26,8 @@ func NewOptions(opts ...Option) Options { | |||||||
| 		o(&options) | 		o(&options) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// create service if its blank | 	if options.Client == nil { | ||||||
| 	if options.Service == nil { | 		WithClient(grpc.NewClient())(&options) | ||||||
| 		WithService(micro.NewService())(&options) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// set namespace if blank | 	// set namespace if blank | ||||||
| @@ -30,6 +35,10 @@ func NewOptions(opts ...Option) Options { | |||||||
| 		WithNamespace("go.micro.api")(&options) | 		WithNamespace("go.micro.api")(&options) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if options.MaxRecvSize == 0 { | ||||||
|  | 		options.MaxRecvSize = DefaultMaxRecvSize | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return options | 	return options | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -47,9 +56,15 @@ func WithRouter(r router.Router) Option { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // WithService specifies a micro.Service | func WithClient(c client.Client) Option { | ||||||
| func WithService(s micro.Service) Option { |  | ||||||
| 	return func(o *Options) { | 	return func(o *Options) { | ||||||
| 		o.Service = s | 		o.Client = c | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithmaxRecvSize specifies max body size | ||||||
|  | func WithMaxRecvSize(size int64) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.MaxRecvSize = size | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,211 +0,0 @@ | |||||||
| // 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 ®istryHandler{ |  | ||||||
| 		opts: options, |  | ||||||
| 		reg:  options.Service.Client().Options().Registry, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -4,23 +4,27 @@ package rpc | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/textproto" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/joncalhoun/qson" | 	jsonpatch "github.com/evanphx/json-patch/v5" | ||||||
| 	"github.com/micro/go-micro/api" | 	"github.com/micro/go-micro/v2/api" | ||||||
| 	"github.com/micro/go-micro/api/handler" | 	"github.com/micro/go-micro/v2/api/handler" | ||||||
| 	proto "github.com/micro/go-micro/api/internal/proto" | 	"github.com/micro/go-micro/v2/api/internal/proto" | ||||||
| 	"github.com/micro/go-micro/client" | 	"github.com/micro/go-micro/v2/client" | ||||||
| 	"github.com/micro/go-micro/client/selector" | 	"github.com/micro/go-micro/v2/client/selector" | ||||||
| 	"github.com/micro/go-micro/codec" | 	"github.com/micro/go-micro/v2/codec" | ||||||
| 	"github.com/micro/go-micro/codec/jsonrpc" | 	"github.com/micro/go-micro/v2/codec/jsonrpc" | ||||||
| 	"github.com/micro/go-micro/codec/protorpc" | 	"github.com/micro/go-micro/v2/codec/protorpc" | ||||||
| 	"github.com/micro/go-micro/errors" | 	"github.com/micro/go-micro/v2/errors" | ||||||
| 	"github.com/micro/go-micro/registry" | 	"github.com/micro/go-micro/v2/logger" | ||||||
| 	"github.com/micro/go-micro/util/ctx" | 	"github.com/micro/go-micro/v2/metadata" | ||||||
|  | 	"github.com/micro/go-micro/v2/registry" | ||||||
|  | 	"github.com/micro/go-micro/v2/util/ctx" | ||||||
|  | 	"github.com/micro/go-micro/v2/util/qson" | ||||||
|  | 	"github.com/oxtoacart/bpool" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -44,6 +48,8 @@ var ( | |||||||
| 		"application/proto-rpc", | 		"application/proto-rpc", | ||||||
| 		"application/octet-stream", | 		"application/octet-stream", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	bufferPool = bpool.NewSizedBufferPool(1024, 8) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type rpcHandler struct { | type rpcHandler struct { | ||||||
| @@ -68,6 +74,13 @@ func strategy(services []*registry.Service) selector.Strategy { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	bsize := handler.DefaultMaxRecvSize | ||||||
|  | 	if h.opts.MaxRecvSize > 0 { | ||||||
|  | 		bsize = h.opts.MaxRecvSize | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.Body = http.MaxBytesReader(w, r.Body, bsize) | ||||||
|  |  | ||||||
| 	defer r.Body.Close() | 	defer r.Body.Close() | ||||||
| 	var service *api.Service | 	var service *api.Service | ||||||
|  |  | ||||||
| @@ -88,12 +101,6 @@ func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 		return | 		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") | 	ct := r.Header.Get("Content-Type") | ||||||
|  |  | ||||||
| 	// Strip charset from Content-Type (like `application/json; charset=UTF-8`) | 	// Strip charset from Content-Type (like `application/json; charset=UTF-8`) | ||||||
| @@ -102,11 +109,42 @@ func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// micro client | 	// micro client | ||||||
| 	c := h.opts.Service.Client() | 	c := h.opts.Client | ||||||
|  |  | ||||||
|  | 	// create context | ||||||
|  | 	cx := ctx.FromRequest(r) | ||||||
|  | 	// get context from http handler wrappers | ||||||
|  | 	md, ok := metadata.FromContext(r.Context()) | ||||||
|  | 	if !ok { | ||||||
|  | 		md = make(metadata.Metadata) | ||||||
|  | 	} | ||||||
|  | 	// fill contex with http headers | ||||||
|  | 	md["Host"] = r.Host | ||||||
|  | 	md["Method"] = r.Method | ||||||
|  | 	// get canonical headers | ||||||
|  | 	for k, _ := range r.Header { | ||||||
|  | 		// may be need to get all values for key like r.Header.Values() provide in go 1.14 | ||||||
|  | 		md[textproto.CanonicalMIMEHeaderKey(k)] = r.Header.Get(k) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// merge context with overwrite | ||||||
|  | 	cx = metadata.MergeContext(cx, md, true) | ||||||
|  |  | ||||||
|  | 	// set merged context to request | ||||||
|  | 	*r = *r.Clone(cx) | ||||||
|  | 	// if stream we currently only support json | ||||||
|  | 	if isStream(r, service) { | ||||||
|  | 		// drop older context as it can have timeouts and create new | ||||||
|  | 		//		md, _ := metadata.FromContext(cx) | ||||||
|  | 		//serveWebsocket(context.TODO(), w, r, service, c) | ||||||
|  | 		serveWebsocket(cx, w, r, service, c) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// create strategy | 	// create strategy | ||||||
| 	so := selector.WithStrategy(strategy(service.Services)) | 	so := selector.WithStrategy(strategy(service.Services)) | ||||||
|  |  | ||||||
|  | 	// walk the standard call path | ||||||
| 	// get payload | 	// get payload | ||||||
| 	br, err := requestPayload(r) | 	br, err := requestPayload(r) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -114,9 +152,6 @@ func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// create context |  | ||||||
| 	cx := ctx.FromRequest(r) |  | ||||||
|  |  | ||||||
| 	var rsp []byte | 	var rsp []byte | ||||||
|  |  | ||||||
| 	switch { | 	switch { | ||||||
| @@ -145,7 +180,12 @@ func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// marshall response | 		// marshall response | ||||||
| 		rsp, _ = response.Marshal() | 		rsp, err = response.Marshal() | ||||||
|  | 		if err != nil { | ||||||
|  | 			writeError(w, r, err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 	default: | 	default: | ||||||
| 		// if json codec is not present set to json | 		// if json codec is not present set to json | ||||||
| 		if !hasCodec(ct, jsonCodecs) { | 		if !hasCodec(ct, jsonCodecs) { | ||||||
| @@ -168,7 +208,6 @@ func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 			&request, | 			&request, | ||||||
| 			client.WithContentType(ct), | 			client.WithContentType(ct), | ||||||
| 		) | 		) | ||||||
|  |  | ||||||
| 		// make the call | 		// make the call | ||||||
| 		if err := c.Call(cx, req, &response, client.WithSelectOption(so)); err != nil { | 		if err := c.Call(cx, req, &response, client.WithSelectOption(so)); err != nil { | ||||||
| 			writeError(w, r, err) | 			writeError(w, r, err) | ||||||
| @@ -176,7 +215,11 @@ func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// marshall response | 		// marshall response | ||||||
| 		rsp, _ = response.MarshalJSON() | 		rsp, err = response.MarshalJSON() | ||||||
|  | 		if err != nil { | ||||||
|  | 			writeError(w, r, err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// write the response | 	// write the response | ||||||
| @@ -200,49 +243,203 @@ func hasCodec(ct string, codecs []string) bool { | |||||||
| // 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 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 | // If the request method is a POST the request body is read and returned | ||||||
| func requestPayload(r *http.Request) ([]byte, error) { | func requestPayload(r *http.Request) ([]byte, error) { | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
| 	// we have to decode json-rpc and proto-rpc because we suck | 	// we have to decode json-rpc and proto-rpc because we suck | ||||||
| 	// well actually because there's no proxy codec right now | 	// well actually because there's no proxy codec right now | ||||||
| 	switch r.Header.Get("Content-Type") { |  | ||||||
| 	case "application/json-rpc": | 	ct := r.Header.Get("Content-Type") | ||||||
|  | 	switch { | ||||||
|  | 	case strings.Contains(ct, "application/json-rpc"): | ||||||
| 		msg := codec.Message{ | 		msg := codec.Message{ | ||||||
| 			Type:   codec.Request, | 			Type:   codec.Request, | ||||||
| 			Header: make(map[string]string), | 			Header: make(map[string]string), | ||||||
| 		} | 		} | ||||||
| 		c := jsonrpc.NewCodec(&buffer{r.Body}) | 		c := jsonrpc.NewCodec(&buffer{r.Body}) | ||||||
| 		if err := c.ReadHeader(&msg, codec.Request); err != nil { | 		if err = c.ReadHeader(&msg, codec.Request); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		var raw json.RawMessage | 		var raw json.RawMessage | ||||||
| 		if err := c.ReadBody(&raw); err != nil { | 		if err = c.ReadBody(&raw); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		return ([]byte)(raw), nil | 		return ([]byte)(raw), nil | ||||||
| 	case "application/proto-rpc", "application/octet-stream": | 	case strings.Contains(ct, "application/proto-rpc"), strings.Contains(ct, "application/octet-stream"): | ||||||
| 		msg := codec.Message{ | 		msg := codec.Message{ | ||||||
| 			Type:   codec.Request, | 			Type:   codec.Request, | ||||||
| 			Header: make(map[string]string), | 			Header: make(map[string]string), | ||||||
| 		} | 		} | ||||||
| 		c := protorpc.NewCodec(&buffer{r.Body}) | 		c := protorpc.NewCodec(&buffer{r.Body}) | ||||||
| 		if err := c.ReadHeader(&msg, codec.Request); err != nil { | 		if err = c.ReadHeader(&msg, codec.Request); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		var raw proto.Message | 		var raw proto.Message | ||||||
| 		if err := c.ReadBody(&raw); err != nil { | 		if err = c.ReadBody(&raw); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		b, _ := raw.Marshal() | 		return raw.Marshal() | ||||||
| 		return b, nil | 	case strings.Contains(ct, "application/www-x-form-urlencoded"): | ||||||
|  | 		r.ParseForm() | ||||||
|  |  | ||||||
|  | 		// generate a new set of values from the form | ||||||
|  | 		vals := make(map[string]string) | ||||||
|  | 		for k, v := range r.Form { | ||||||
|  | 			vals[k] = strings.Join(v, ",") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// marshal | ||||||
|  | 		return json.Marshal(vals) | ||||||
|  | 		// TODO: application/grpc | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// otherwise as per usual | 	// otherwise as per usual | ||||||
|  | 	ctx := r.Context() | ||||||
|  | 	// dont user meadata.FromContext as it mangles names | ||||||
|  | 	md, ok := metadata.FromContext(ctx) | ||||||
|  | 	if !ok { | ||||||
|  | 		md = make(map[string]string) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// allocate maximum | ||||||
|  | 	matches := make(map[string]interface{}, len(md)) | ||||||
|  | 	bodydst := "" | ||||||
|  |  | ||||||
|  | 	// get fields from url path | ||||||
|  | 	for k, v := range md { | ||||||
|  | 		k = strings.ToLower(k) | ||||||
|  | 		// filter own keys | ||||||
|  | 		if strings.HasPrefix(k, "x-api-field-") { | ||||||
|  | 			matches[strings.TrimPrefix(k, "x-api-field-")] = v | ||||||
|  | 			delete(md, k) | ||||||
|  | 		} else if k == "x-api-body" { | ||||||
|  | 			bodydst = v | ||||||
|  | 			delete(md, k) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// map of all fields | ||||||
|  | 	req := make(map[string]interface{}, len(md)) | ||||||
|  |  | ||||||
|  | 	// get fields from url values | ||||||
|  | 	if len(r.URL.RawQuery) > 0 { | ||||||
|  | 		umd := make(map[string]interface{}) | ||||||
|  | 		err = qson.Unmarshal(&umd, r.URL.RawQuery) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		for k, v := range umd { | ||||||
|  | 			matches[k] = v | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// restore context without fields | ||||||
|  | 	*r = *r.Clone(metadata.NewContext(ctx, md)) | ||||||
|  |  | ||||||
|  | 	for k, v := range matches { | ||||||
|  | 		ps := strings.Split(k, ".") | ||||||
|  | 		if len(ps) == 1 { | ||||||
|  | 			req[k] = v | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		em := make(map[string]interface{}) | ||||||
|  | 		em[ps[len(ps)-1]] = v | ||||||
|  | 		for i := len(ps) - 2; i > 0; i-- { | ||||||
|  | 			nm := make(map[string]interface{}) | ||||||
|  | 			nm[ps[i]] = em | ||||||
|  | 			em = nm | ||||||
|  | 		} | ||||||
|  | 		if vm, ok := req[ps[0]]; ok { | ||||||
|  | 			// nested map | ||||||
|  | 			nm := vm.(map[string]interface{}) | ||||||
|  | 			for vk, vv := range em { | ||||||
|  | 				nm[vk] = vv | ||||||
|  | 			} | ||||||
|  | 			req[ps[0]] = nm | ||||||
|  | 		} else { | ||||||
|  | 			req[ps[0]] = em | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	pathbuf := []byte("{}") | ||||||
|  | 	if len(req) > 0 { | ||||||
|  | 		pathbuf, err = json.Marshal(req) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	urlbuf := []byte("{}") | ||||||
|  | 	out, err := jsonpatch.MergeMergePatches(urlbuf, pathbuf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	switch r.Method { | 	switch r.Method { | ||||||
| 	case "GET": | 	case "GET": | ||||||
| 		if len(r.URL.RawQuery) > 0 { | 		// empty response | ||||||
| 			return qson.ToJSON(r.URL.RawQuery) | 		if strings.Contains(ct, "application/json") && string(out) == "{}" { | ||||||
|  | 			return out, nil | ||||||
|  | 		} else if string(out) == "{}" && !strings.Contains(ct, "application/json") { | ||||||
|  | 			return []byte{}, nil | ||||||
| 		} | 		} | ||||||
| 	case "PATCH", "POST": | 		return out, nil | ||||||
| 		return ioutil.ReadAll(r.Body) | 	case "PATCH", "POST", "PUT", "DELETE": | ||||||
|  | 		bodybuf := []byte("{}") | ||||||
|  | 		buf := bufferPool.Get() | ||||||
|  | 		defer bufferPool.Put(buf) | ||||||
|  | 		if _, err := buf.ReadFrom(r.Body); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		if b := buf.Bytes(); len(b) > 0 { | ||||||
|  | 			bodybuf = b | ||||||
|  | 		} | ||||||
|  | 		if bodydst == "" || bodydst == "*" { | ||||||
|  | 			if out, err = jsonpatch.MergeMergePatches(out, bodybuf); err == nil { | ||||||
|  | 				return out, nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		var jsonbody map[string]interface{} | ||||||
|  | 		if json.Valid(bodybuf) { | ||||||
|  | 			if err = json.Unmarshal(bodybuf, &jsonbody); err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		dstmap := make(map[string]interface{}) | ||||||
|  | 		ps := strings.Split(bodydst, ".") | ||||||
|  | 		if len(ps) == 1 { | ||||||
|  | 			if jsonbody != nil { | ||||||
|  | 				dstmap[ps[0]] = jsonbody | ||||||
|  | 			} else { | ||||||
|  | 				// old unexpected behaviour | ||||||
|  | 				dstmap[ps[0]] = bodybuf | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			em := make(map[string]interface{}) | ||||||
|  | 			if jsonbody != nil { | ||||||
|  | 				em[ps[len(ps)-1]] = jsonbody | ||||||
|  | 			} else { | ||||||
|  | 				// old unexpected behaviour | ||||||
|  | 				em[ps[len(ps)-1]] = bodybuf | ||||||
|  | 			} | ||||||
|  | 			for i := len(ps) - 2; i > 0; i-- { | ||||||
|  | 				nm := make(map[string]interface{}) | ||||||
|  | 				nm[ps[i]] = em | ||||||
|  | 				em = nm | ||||||
|  | 			} | ||||||
|  | 			dstmap[ps[0]] = em | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bodyout, err := json.Marshal(dstmap) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if out, err = jsonpatch.MergeMergePatches(out, bodyout); err == nil { | ||||||
|  | 			return out, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		//fallback to previous unknown behaviour | ||||||
|  | 		return bodybuf, nil | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return []byte{}, nil | 	return []byte{}, nil | ||||||
| @@ -274,7 +471,12 @@ func writeError(w http.ResponseWriter, r *http.Request, err error) { | |||||||
| 		w.Header().Set("grpc-message", ce.Detail) | 		w.Header().Set("grpc-message", ce.Detail) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	w.Write([]byte(ce.Error())) | 	_, werr := w.Write([]byte(ce.Error())) | ||||||
|  | 	if werr != nil { | ||||||
|  | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Error(werr) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) { | func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) { | ||||||
| @@ -289,8 +491,19 @@ func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) { | |||||||
| 		w.Header().Set("grpc-message", "") | 		w.Header().Set("grpc-message", "") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// write 204 status if rsp is nil | ||||||
|  | 	if len(rsp) == 0 { | ||||||
|  | 		w.WriteHeader(http.StatusNoContent) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// write response | 	// write response | ||||||
| 	w.Write(rsp) | 	_, err := w.Write(rsp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Error(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewHandler(opts ...handler.Option) handler.Handler { | func NewHandler(opts ...handler.Option) handler.Handler { | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/golang/protobuf/proto" | 	"github.com/golang/protobuf/proto" | ||||||
| 	go_api "github.com/micro/go-micro/api/proto" | 	go_api "github.com/micro/go-micro/v2/api/proto" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestRequestPayloadFromRequest(t *testing.T) { | func TestRequestPayloadFromRequest(t *testing.T) { | ||||||
| @@ -27,6 +27,23 @@ func TestRequestPayloadFromRequest(t *testing.T) { | |||||||
| 		t.Fatal("Failed to marshal proto to JSON ", err) | 		t.Fatal("Failed to marshal proto to JSON ", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	jsonUrlBytes := []byte(`{"key1":"val1","key2":"val2","name":"Test"}`) | ||||||
|  |  | ||||||
|  | 	t.Run("extracting a json from a POST request with url params", func(t *testing.T) { | ||||||
|  | 		r, err := http.NewRequest("POST", "http://localhost/my/path?key1=val1&key2=val2", 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(jsonUrlBytes) { | ||||||
|  | 			t.Fatalf("Expected %v and %v to match", string(extByte), jsonUrlBytes) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	t.Run("extracting a proto from a POST request", func(t *testing.T) { | 	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)) | 		r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(protoBytes)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
							
								
								
									
										255
									
								
								api/handler/rpc/stream.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								api/handler/rpc/stream.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | |||||||
|  | package rpc | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/gobwas/httphead" | ||||||
|  | 	"github.com/gobwas/ws" | ||||||
|  | 	"github.com/gobwas/ws/wsutil" | ||||||
|  | 	"github.com/micro/go-micro/v2/api" | ||||||
|  | 	"github.com/micro/go-micro/v2/client" | ||||||
|  | 	"github.com/micro/go-micro/v2/client/selector" | ||||||
|  | 	raw "github.com/micro/go-micro/v2/codec/bytes" | ||||||
|  | 	"github.com/micro/go-micro/v2/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // serveWebsocket will stream rpc back over websockets assuming json | ||||||
|  | func serveWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, service *api.Service, c client.Client) { | ||||||
|  | 	var op ws.OpCode | ||||||
|  |  | ||||||
|  | 	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] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check proto from request | ||||||
|  | 	switch ct { | ||||||
|  | 	case "application/json": | ||||||
|  | 		op = ws.OpText | ||||||
|  | 	default: | ||||||
|  | 		op = ws.OpBinary | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hdr := make(http.Header) | ||||||
|  | 	if proto, ok := r.Header["Sec-WebSocket-Protocol"]; ok { | ||||||
|  | 		for _, p := range proto { | ||||||
|  | 			switch p { | ||||||
|  | 			case "binary": | ||||||
|  | 				hdr["Sec-WebSocket-Protocol"] = []string{"binary"} | ||||||
|  | 				op = ws.OpBinary | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	payload, err := requestPayload(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Error(err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	upgrader := ws.HTTPUpgrader{Timeout: 5 * time.Second, | ||||||
|  | 		Protocol: func(proto string) bool { | ||||||
|  | 			if strings.Contains(proto, "binary") { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 			// fallback to support all protocols now | ||||||
|  | 			return true | ||||||
|  | 		}, | ||||||
|  | 		Extension: func(httphead.Option) bool { | ||||||
|  | 			// disable extensions for compatibility | ||||||
|  | 			return false | ||||||
|  | 		}, | ||||||
|  | 		Header: hdr, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	conn, rw, _, err := upgrader.Upgrade(r, w) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Error(err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defer func() { | ||||||
|  | 		if err := conn.Close(); err != nil { | ||||||
|  | 			if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 				logger.Error(err) | ||||||
|  | 			} | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	var request interface{} | ||||||
|  | 	if !bytes.Equal(payload, []byte(`{}`)) { | ||||||
|  | 		switch ct { | ||||||
|  | 		case "application/json", "": | ||||||
|  | 			m := json.RawMessage(payload) | ||||||
|  | 			request = &m | ||||||
|  | 		default: | ||||||
|  | 			request = &raw.Frame{Data: payload} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// we always need to set content type for message | ||||||
|  | 	if ct == "" { | ||||||
|  | 		ct = "application/json" | ||||||
|  | 	} | ||||||
|  | 	req := c.NewRequest( | ||||||
|  | 		service.Name, | ||||||
|  | 		service.Endpoint.Name, | ||||||
|  | 		request, | ||||||
|  | 		client.WithContentType(ct), | ||||||
|  | 		client.StreamingRequest(), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	so := selector.WithStrategy(strategy(service.Services)) | ||||||
|  | 	// create a new stream | ||||||
|  | 	stream, err := c.Stream(ctx, req, client.WithSelectOption(so)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Error(err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if request != nil { | ||||||
|  | 		if err = stream.Send(request); err != nil { | ||||||
|  | 			if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 				logger.Error(err) | ||||||
|  | 			} | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go writeLoop(rw, stream) | ||||||
|  |  | ||||||
|  | 	rsp := stream.Response() | ||||||
|  |  | ||||||
|  | 	// receive from stream and send to client | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		case <-stream.Context().Done(): | ||||||
|  | 			return | ||||||
|  | 		default: | ||||||
|  | 			// read backend response body | ||||||
|  | 			buf, err := rsp.Read() | ||||||
|  | 			if err != nil { | ||||||
|  | 				// wants to avoid import  grpc/status.Status | ||||||
|  | 				if strings.Contains(err.Error(), "context canceled") { | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Error(err) | ||||||
|  | 				} | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// write the response | ||||||
|  | 			if err := wsutil.WriteServerMessage(rw, op, buf); err != nil { | ||||||
|  | 				if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Error(err) | ||||||
|  | 				} | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if err = rw.Flush(); err != nil { | ||||||
|  | 				if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Error(err) | ||||||
|  | 				} | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // writeLoop | ||||||
|  | func writeLoop(rw io.ReadWriter, stream client.Stream) { | ||||||
|  | 	// close stream when done | ||||||
|  | 	defer stream.Close() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-stream.Context().Done(): | ||||||
|  | 			return | ||||||
|  | 		default: | ||||||
|  | 			buf, op, err := wsutil.ReadClientData(rw) | ||||||
|  | 			if err != nil { | ||||||
|  | 				if wserr, ok := err.(wsutil.ClosedError); ok { | ||||||
|  | 					switch wserr.Code { | ||||||
|  | 					case ws.StatusNormalClosure, ws.StatusNoStatusRcvd: | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Error(err) | ||||||
|  | 				} | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			switch op { | ||||||
|  | 			default: | ||||||
|  | 				// not relevant | ||||||
|  | 				continue | ||||||
|  | 			case ws.OpText, ws.OpBinary: | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			// send to backend | ||||||
|  | 			// default to trying json | ||||||
|  | 			// if the extracted payload isn't empty lets use it | ||||||
|  | 			request := &raw.Frame{Data: buf} | ||||||
|  | 			if err := stream.Send(request); err != nil { | ||||||
|  | 				if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Error(err) | ||||||
|  | 				} | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isStream(r *http.Request, srv *api.Service) bool { | ||||||
|  | 	// check if it's a web socket | ||||||
|  | 	if !isWebSocket(r) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	// check if the endpoint supports streaming | ||||||
|  | 	for _, service := range srv.Services { | ||||||
|  | 		for _, ep := range service.Endpoints { | ||||||
|  | 			// skip if it doesn't match the name | ||||||
|  | 			if ep.Name != srv.Endpoint.Name { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			// matched if the name | ||||||
|  | 			if v := ep.Metadata["stream"]; v == "true" { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | } | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| // 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" |  | ||||||
| } |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| // 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" |  | ||||||
| } |  | ||||||
| @@ -11,9 +11,9 @@ import ( | |||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api" | 	"github.com/micro/go-micro/v2/api" | ||||||
| 	"github.com/micro/go-micro/api/handler" | 	"github.com/micro/go-micro/v2/api/handler" | ||||||
| 	"github.com/micro/go-micro/client/selector" | 	"github.com/micro/go-micro/v2/client/selector" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| // Code generated by protoc-gen-go. DO NOT EDIT. | // Code generated by protoc-gen-go. DO NOT EDIT. | ||||||
| // source: github.com/micro/go-micro/api/proto/api.proto | // source: api/proto/api.proto | ||||||
|  |  | ||||||
| package go_api | package go_api | ||||||
|  |  | ||||||
| @@ -32,7 +32,7 @@ func (m *Pair) Reset()         { *m = Pair{} } | |||||||
| func (m *Pair) String() string { return proto.CompactTextString(m) } | func (m *Pair) String() string { return proto.CompactTextString(m) } | ||||||
| func (*Pair) ProtoMessage()    {} | func (*Pair) ProtoMessage()    {} | ||||||
| func (*Pair) Descriptor() ([]byte, []int) { | func (*Pair) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_7b6696ef87ec1943, []int{0} | 	return fileDescriptor_2df576b66d12087a, []int{0} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *Pair) XXX_Unmarshal(b []byte) error { | func (m *Pair) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -86,7 +86,7 @@ func (m *Request) Reset()         { *m = Request{} } | |||||||
| func (m *Request) String() string { return proto.CompactTextString(m) } | func (m *Request) String() string { return proto.CompactTextString(m) } | ||||||
| func (*Request) ProtoMessage()    {} | func (*Request) ProtoMessage()    {} | ||||||
| func (*Request) Descriptor() ([]byte, []int) { | func (*Request) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_7b6696ef87ec1943, []int{1} | 	return fileDescriptor_2df576b66d12087a, []int{1} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *Request) XXX_Unmarshal(b []byte) error { | func (m *Request) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -171,7 +171,7 @@ func (m *Response) Reset()         { *m = Response{} } | |||||||
| func (m *Response) String() string { return proto.CompactTextString(m) } | func (m *Response) String() string { return proto.CompactTextString(m) } | ||||||
| func (*Response) ProtoMessage()    {} | func (*Response) ProtoMessage()    {} | ||||||
| func (*Response) Descriptor() ([]byte, []int) { | func (*Response) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_7b6696ef87ec1943, []int{2} | 	return fileDescriptor_2df576b66d12087a, []int{2} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *Response) XXX_Unmarshal(b []byte) error { | func (m *Response) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -235,7 +235,7 @@ func (m *Event) Reset()         { *m = Event{} } | |||||||
| func (m *Event) String() string { return proto.CompactTextString(m) } | func (m *Event) String() string { return proto.CompactTextString(m) } | ||||||
| func (*Event) ProtoMessage()    {} | func (*Event) ProtoMessage()    {} | ||||||
| func (*Event) Descriptor() ([]byte, []int) { | func (*Event) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_7b6696ef87ec1943, []int{3} | 	return fileDescriptor_2df576b66d12087a, []int{3} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *Event) XXX_Unmarshal(b []byte) error { | func (m *Event) XXX_Unmarshal(b []byte) error { | ||||||
| @@ -303,36 +303,33 @@ func init() { | |||||||
| 	proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Event.HeaderEntry") | 	proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Event.HeaderEntry") | ||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func init() { proto.RegisterFile("api/proto/api.proto", fileDescriptor_2df576b66d12087a) } | ||||||
| 	proto.RegisterFile("github.com/micro/go-micro/api/proto/api.proto", fileDescriptor_7b6696ef87ec1943) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var fileDescriptor_7b6696ef87ec1943 = []byte{ | var fileDescriptor_2df576b66d12087a = []byte{ | ||||||
| 	// 408 bytes of a gzipped FileDescriptorProto | 	// 393 bytes of a gzipped FileDescriptorProto | ||||||
| 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0x4d, 0x8f, 0xd3, 0x30, | 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xcd, 0xce, 0xd3, 0x30, | ||||||
| 	0x10, 0x55, 0xe2, 0x24, 0xbb, 0x99, 0x22, 0x84, 0x7c, 0x40, 0x66, 0x59, 0xa1, 0x2a, 0xa7, 0x0a, | 	0x10, 0x54, 0xe2, 0x24, 0x6d, 0xb6, 0x08, 0x21, 0x23, 0x21, 0x53, 0x2a, 0x54, 0xe5, 0x54, 0x21, | ||||||
| 	0xa9, 0x29, 0xec, 0x72, 0x40, 0x5c, 0xa1, 0x5a, 0x8e, 0x2b, 0xff, 0x03, 0x77, 0x63, 0x25, 0x16, | 	0x91, 0x42, 0xcb, 0x01, 0x71, 0x85, 0xaa, 0x1c, 0x2b, 0xbf, 0x81, 0xab, 0x58, 0x6d, 0x44, 0x13, | ||||||
| 	0x4d, 0x1c, 0x62, 0xa7, 0x52, 0x7f, 0x1c, 0x07, 0x7e, 0x06, 0xff, 0x06, 0x79, 0xec, 0x7e, 0x50, | 	0x9b, 0xd8, 0xa9, 0xd4, 0x87, 0xe3, 0xc0, 0x63, 0xf0, 0x36, 0xc8, 0x1b, 0xf7, 0xe7, 0xab, 0xfa, | ||||||
| 	0x95, 0x0b, 0xf4, 0xf6, 0x62, 0xbf, 0x79, 0xf3, 0xe6, 0x8d, 0x03, 0xf3, 0x5a, 0xd9, 0x66, 0x5c, | 	0x5d, 0xbe, 0xaf, 0xb7, 0x89, 0x3d, 0x3b, 0x3b, 0x3b, 0xeb, 0xc0, 0x6b, 0xa1, 0xcb, 0xa9, 0x6e, | ||||||
| 	0x95, 0x4f, 0xba, 0x5d, 0xb4, 0xea, 0x69, 0xd0, 0x8b, 0x5a, 0xcf, 0x3d, 0x10, 0xbd, 0x5a, 0xf4, | 	0x94, 0x55, 0x53, 0xa1, 0xcb, 0x1c, 0x11, 0x4d, 0x36, 0x2a, 0x17, 0xba, 0xcc, 0x3e, 0x41, 0xb4, | ||||||
| 	0x83, 0xb6, 0x88, 0x4a, 0x44, 0x34, 0xab, 0x75, 0x29, 0x7a, 0x55, 0xbc, 0x83, 0xe4, 0x51, 0xa8, | 	0x12, 0x65, 0x43, 0x5f, 0x01, 0xf9, 0x25, 0x0f, 0x2c, 0x18, 0x07, 0x93, 0x94, 0x3b, 0x48, 0xdf, | ||||||
| 	0x81, 0xbe, 0x00, 0xf2, 0x4d, 0x6e, 0x59, 0x34, 0x8d, 0x66, 0x39, 0x77, 0x90, 0xbe, 0x84, 0x6c, | 	0x40, 0xb2, 0x17, 0xbb, 0x56, 0x1a, 0x16, 0x8e, 0xc9, 0x24, 0xe5, 0xfe, 0x2b, 0xfb, 0x4b, 0xa0, | ||||||
| 	0x23, 0xd6, 0xa3, 0x34, 0x2c, 0x9e, 0x92, 0x59, 0xce, 0xc3, 0x57, 0xf1, 0x93, 0xc0, 0x15, 0x97, | 	0xc7, 0xe5, 0xef, 0x56, 0x1a, 0xeb, 0x38, 0x95, 0xb4, 0x5b, 0x55, 0xf8, 0x42, 0xff, 0x45, 0x29, | ||||||
| 	0xdf, 0x47, 0x69, 0xac, 0xe3, 0xb4, 0xd2, 0x36, 0xba, 0x0a, 0x85, 0xe1, 0x8b, 0x52, 0x48, 0x7a, | 	0x44, 0x5a, 0xd8, 0x2d, 0x0b, 0xf1, 0x14, 0x31, 0x9d, 0x43, 0xb2, 0x95, 0xa2, 0x90, 0x0d, 0x23, | ||||||
| 	0x61, 0x1b, 0x16, 0xe3, 0x29, 0x62, 0x7a, 0x0f, 0x59, 0x23, 0x45, 0x25, 0x07, 0x46, 0xa6, 0x64, | 	0x63, 0x32, 0x19, 0xcc, 0xde, 0xe5, 0x9d, 0x85, 0xdc, 0x8b, 0xe5, 0x3f, 0xf1, 0x76, 0x51, 0xdb, | ||||||
| 	0x36, 0xb9, 0x7b, 0x5d, 0x7a, 0x0b, 0x65, 0x10, 0x2b, 0xbf, 0xe2, 0xed, 0xb2, 0xb3, 0xc3, 0x96, | 	0xe6, 0xc0, 0x3d, 0x95, 0x7e, 0x00, 0xb2, 0x91, 0x96, 0x45, 0x58, 0xc1, 0xae, 0x2b, 0x96, 0xd2, | ||||||
| 	0x07, 0x2a, 0x7d, 0x0b, 0xa4, 0x96, 0x96, 0x25, 0x58, 0xc1, 0x4e, 0x2b, 0x1e, 0xa4, 0xf5, 0x74, | 	0x76, 0x74, 0x47, 0xa2, 0x1f, 0x21, 0xd2, 0xca, 0x58, 0x16, 0x23, 0xf9, 0xed, 0x35, 0x79, 0xa5, | ||||||
| 	0x47, 0xa2, 0x73, 0x48, 0x7a, 0x6d, 0x2c, 0x4b, 0x91, 0xfc, 0xea, 0x94, 0xfc, 0xa8, 0x4d, 0x60, | 	0x8c, 0x67, 0x23, 0xcd, 0x79, 0x5c, 0xab, 0xe2, 0xc0, 0x92, 0xce, 0xa3, 0xc3, 0x2e, 0x85, 0xb6, | ||||||
| 	0x23, 0xcd, 0x79, 0x5c, 0xe9, 0x6a, 0xcb, 0x32, 0xef, 0xd1, 0x61, 0x97, 0xc2, 0x38, 0xac, 0xd9, | 	0xd9, 0xb1, 0x5e, 0x97, 0x42, 0xdb, 0xec, 0x86, 0x4b, 0x18, 0x5c, 0xf8, 0xba, 0x11, 0x53, 0x06, | ||||||
| 	0x95, 0x4f, 0x61, 0x1c, 0xd6, 0x37, 0x0f, 0x30, 0x39, 0xf2, 0x75, 0x26, 0xa6, 0x02, 0x52, 0x0c, | 	0x31, 0x06, 0x83, 0xb3, 0x0e, 0x66, 0x2f, 0x8e, 0x6d, 0x5d, 0xaa, 0xbc, 0xbb, 0xfa, 0x16, 0x7e, | ||||||
| 	0x06, 0x67, 0x9d, 0xdc, 0x3d, 0xdb, 0xb5, 0x75, 0xa9, 0x72, 0x7f, 0xf5, 0x29, 0xfe, 0x18, 0xdd, | 	0x0d, 0x86, 0x3f, 0xa0, 0x7f, 0xb4, 0xfb, 0x0c, 0x95, 0x05, 0xa4, 0xa7, 0x39, 0x9e, 0x2e, 0x93, | ||||||
| 	0x7c, 0x81, 0xeb, 0x9d, 0xdd, 0xff, 0x50, 0x59, 0x42, 0xbe, 0x9f, 0xe3, 0xdf, 0x65, 0x8a, 0x1f, | 	0xfd, 0x09, 0xa0, 0xcf, 0xa5, 0xd1, 0xaa, 0x36, 0x92, 0xbe, 0x07, 0x30, 0x56, 0xd8, 0xd6, 0x7c, | ||||||
| 	0x11, 0x5c, 0x73, 0x69, 0x7a, 0xdd, 0x19, 0x49, 0xdf, 0x00, 0x18, 0x2b, 0xec, 0x68, 0x3e, 0xeb, | 	0x57, 0x85, 0x44, 0xb5, 0x98, 0x5f, 0x9c, 0xd0, 0x2f, 0xa7, 0xc5, 0x85, 0x98, 0xec, 0xe8, 0x9c, | ||||||
| 	0x4a, 0xa2, 0x5a, 0xca, 0x8f, 0x4e, 0xe8, 0x87, 0xfd, 0xe2, 0x62, 0x4c, 0xf6, 0xf6, 0x90, 0xac, | 	0x6c, 0xa7, 0x70, 0x73, 0x73, 0xc7, 0x78, 0xc9, 0x39, 0xde, 0xbb, 0x85, 0x99, 0xfd, 0x0b, 0x20, | ||||||
| 	0x57, 0x38, 0xbb, 0xb9, 0x5d, 0xbc, 0xe4, 0x10, 0xef, 0xc5, 0xc2, 0x2c, 0x7e, 0x45, 0x90, 0x2e, | 	0x5e, 0xec, 0x65, 0x8d, 0x5b, 0xac, 0x45, 0x25, 0xbd, 0x08, 0x62, 0xfa, 0x12, 0xc2, 0xb2, 0xf0, | ||||||
| 	0x37, 0xb2, 0xc3, 0x2d, 0x76, 0xa2, 0x95, 0x41, 0x04, 0x31, 0x7d, 0x0e, 0xb1, 0xaa, 0xc2, 0xdb, | 	0x6f, 0x2f, 0x2c, 0x0b, 0x3a, 0x82, 0xd4, 0x96, 0x95, 0x34, 0x56, 0x54, 0x1a, 0xfd, 0x10, 0x7e, | ||||||
| 	0x8b, 0x55, 0x45, 0x6f, 0x21, 0xb7, 0xaa, 0x95, 0xc6, 0x8a, 0xb6, 0x47, 0x3f, 0x84, 0x1f, 0x0e, | 	0x3e, 0xa0, 0x9f, 0x4f, 0xe3, 0x45, 0x0f, 0x1f, 0x0e, 0x36, 0x78, 0x6c, 0xb6, 0x42, 0x58, 0xc1, | ||||||
| 	0xe8, 0xfb, 0xfd, 0x78, 0xc9, 0x9f, 0x0f, 0x07, 0x1b, 0xfc, 0x6d, 0xb6, 0x4a, 0x58, 0xc1, 0x52, | 	0xe2, 0xae, 0xa9, 0xc3, 0x77, 0x9b, 0x6d, 0x9d, 0xe0, 0x0f, 0x3a, 0xff, 0x1f, 0x00, 0x00, 0xff, | ||||||
| 	0xdf, 0xd4, 0xe1, 0x8b, 0xcd, 0xb6, 0xca, 0xf0, 0x07, 0xbd, 0xff, 0x1d, 0x00, 0x00, 0xff, 0xff, | 	0xff, 0xd4, 0x6d, 0x70, 0x51, 0xb7, 0x03, 0x00, 0x00, | ||||||
| 	0x97, 0xf3, 0x59, 0x6e, 0xd1, 0x03, 0x00, 0x00, |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| // Code generated by protoc-gen-micro. DO NOT EDIT. | // Code generated by protoc-gen-micro. DO NOT EDIT. | ||||||
| // source: github.com/micro/go-micro/api/proto/api.proto | // source: api/proto/api.proto | ||||||
|  |  | ||||||
| package go_api | package go_api | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api/resolver" | 	"github.com/micro/go-micro/v2/api/resolver" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Resolver struct{} | type Resolver struct{} | ||||||
|   | |||||||
| @@ -4,10 +4,12 @@ package host | |||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api/resolver" | 	"github.com/micro/go-micro/v2/api/resolver" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Resolver struct{} | type Resolver struct { | ||||||
|  | 	opts resolver.Options | ||||||
|  | } | ||||||
|  |  | ||||||
| func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) { | func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) { | ||||||
| 	return &resolver.Endpoint{ | 	return &resolver.Endpoint{ | ||||||
| @@ -23,5 +25,5 @@ func (r *Resolver) String() string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func NewResolver(opts ...resolver.Option) resolver.Resolver { | func NewResolver(opts ...resolver.Option) resolver.Resolver { | ||||||
| 	return &Resolver{} | 	return &Resolver{opts: resolver.NewOptions(opts...)} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,45 +0,0 @@ | |||||||
| // 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...), |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,90 +0,0 @@ | |||||||
| 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 |  | ||||||
| } |  | ||||||
| @@ -1,130 +0,0 @@ | |||||||
| 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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,11 +1,22 @@ | |||||||
| package resolver | package resolver | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/auth" | ||||||
|  | ) | ||||||
|  |  | ||||||
| // NewOptions returns new initialised options | // NewOptions returns new initialised options | ||||||
| func NewOptions(opts ...Option) Options { | func NewOptions(opts ...Option) Options { | ||||||
| 	var options Options | 	var options Options | ||||||
| 	for _, o := range opts { | 	for _, o := range opts { | ||||||
| 		o(&options) | 		o(&options) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if options.Namespace == nil { | ||||||
|  | 		options.Namespace = StaticNamespace(auth.DefaultNamespace) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return options | 	return options | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -16,8 +27,8 @@ func WithHandler(h string) Option { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // WithNamespace sets the namespace being used | // WithNamespace sets the function which determines the namespace for a request | ||||||
| func WithNamespace(n string) Option { | func WithNamespace(n func(*http.Request) string) Option { | ||||||
| 	return func(o *Options) { | 	return func(o *Options) { | ||||||
| 		o.Namespace = n | 		o.Namespace = n | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -2,22 +2,26 @@ | |||||||
| package path | package path | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api/resolver" | 	"github.com/micro/go-micro/v2/api/resolver" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Resolver struct{} | type Resolver struct { | ||||||
|  | 	opts resolver.Options | ||||||
|  | } | ||||||
|  |  | ||||||
| func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) { | func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) { | ||||||
| 	if req.URL.Path == "/" { | 	if req.URL.Path == "/" { | ||||||
| 		return nil, errors.New("unknown name") | 		return nil, resolver.ErrNotFound | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	parts := strings.Split(req.URL.Path[1:], "/") | 	parts := strings.Split(req.URL.Path[1:], "/") | ||||||
|  | 	ns := r.opts.Namespace(req) | ||||||
|  |  | ||||||
| 	return &resolver.Endpoint{ | 	return &resolver.Endpoint{ | ||||||
| 		Name:   parts[0], | 		Name:   ns + "." + parts[0], | ||||||
| 		Host:   req.Host, | 		Host:   req.Host, | ||||||
| 		Method: req.Method, | 		Method: req.Method, | ||||||
| 		Path:   req.URL.Path, | 		Path:   req.URL.Path, | ||||||
| @@ -29,5 +33,5 @@ func (r *Resolver) String() string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func NewResolver(opts ...resolver.Option) resolver.Resolver { | func NewResolver(opts ...resolver.Option) resolver.Resolver { | ||||||
| 	return &Resolver{} | 	return &Resolver{opts: resolver.NewOptions(opts...)} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,9 +2,15 @@ | |||||||
| package resolver | package resolver | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	ErrNotFound    = errors.New("not found") | ||||||
|  | 	ErrInvalidPath = errors.New("invalid path") | ||||||
|  | ) | ||||||
|  |  | ||||||
| // Resolver resolves requests to endpoints | // Resolver resolves requests to endpoints | ||||||
| type Resolver interface { | type Resolver interface { | ||||||
| 	Resolve(r *http.Request) (*Endpoint, error) | 	Resolve(r *http.Request) (*Endpoint, error) | ||||||
| @@ -25,7 +31,14 @@ type Endpoint struct { | |||||||
|  |  | ||||||
| type Options struct { | type Options struct { | ||||||
| 	Handler   string | 	Handler   string | ||||||
| 	Namespace string | 	Namespace func(*http.Request) string | ||||||
| } | } | ||||||
|  |  | ||||||
| type Option func(o *Options) | type Option func(o *Options) | ||||||
|  |  | ||||||
|  | // StaticNamespace returns the same namespace for each request | ||||||
|  | func StaticNamespace(ns string) func(*http.Request) string { | ||||||
|  | 	return func(*http.Request) string { | ||||||
|  | 		return ns | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -7,10 +7,16 @@ import ( | |||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api/resolver" | 	"github.com/micro/go-micro/v2/api/resolver" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Resolver struct{} | func NewResolver(opts ...resolver.Option) resolver.Resolver { | ||||||
|  | 	return &Resolver{opts: resolver.NewOptions(opts...)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Resolver struct { | ||||||
|  | 	opts resolver.Options | ||||||
|  | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	re = regexp.MustCompile("^v[0-9]+$") | 	re = regexp.MustCompile("^v[0-9]+$") | ||||||
| @@ -22,10 +28,9 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	parts := strings.Split(req.URL.Path[1:], "/") | 	parts := strings.Split(req.URL.Path[1:], "/") | ||||||
|  |  | ||||||
| 	if len(parts) == 1 { | 	if len(parts) == 1 { | ||||||
| 		return &resolver.Endpoint{ | 		return &resolver.Endpoint{ | ||||||
| 			Name:   parts[0], | 			Name:   r.withNamespace(req, parts...), | ||||||
| 			Host:   req.Host, | 			Host:   req.Host, | ||||||
| 			Method: req.Method, | 			Method: req.Method, | ||||||
| 			Path:   req.URL.Path, | 			Path:   req.URL.Path, | ||||||
| @@ -35,7 +40,7 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) { | |||||||
| 	// /v1/foo | 	// /v1/foo | ||||||
| 	if re.MatchString(parts[0]) { | 	if re.MatchString(parts[0]) { | ||||||
| 		return &resolver.Endpoint{ | 		return &resolver.Endpoint{ | ||||||
| 			Name:   parts[1], | 			Name:   r.withNamespace(req, parts[0:2]...), | ||||||
| 			Host:   req.Host, | 			Host:   req.Host, | ||||||
| 			Method: req.Method, | 			Method: req.Method, | ||||||
| 			Path:   req.URL.Path, | 			Path:   req.URL.Path, | ||||||
| @@ -43,7 +48,7 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &resolver.Endpoint{ | 	return &resolver.Endpoint{ | ||||||
| 		Name:   parts[0], | 		Name:   r.withNamespace(req, parts[0]), | ||||||
| 		Host:   req.Host, | 		Host:   req.Host, | ||||||
| 		Method: req.Method, | 		Method: req.Method, | ||||||
| 		Path:   req.URL.Path, | 		Path:   req.URL.Path, | ||||||
| @@ -54,6 +59,11 @@ func (r *Resolver) String() string { | |||||||
| 	return "path" | 	return "path" | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewResolver(opts ...resolver.Option) resolver.Resolver { | func (r *Resolver) withNamespace(req *http.Request, parts ...string) string { | ||||||
| 	return &Resolver{} | 	ns := r.opts.Namespace(req) | ||||||
|  | 	if len(ns) == 0 { | ||||||
|  | 		return strings.Join(parts, ".") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return strings.Join(append([]string{ns}, parts...), ".") | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,12 @@ | |||||||
| package router | package router | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/micro/go-micro/api/resolver" | 	"github.com/micro/go-micro/v2/api/resolver" | ||||||
| 	"github.com/micro/go-micro/api/resolver/micro" | 	"github.com/micro/go-micro/v2/api/resolver/vpath" | ||||||
| 	"github.com/micro/go-micro/config/cmd" | 	"github.com/micro/go-micro/v2/registry" | ||||||
| 	"github.com/micro/go-micro/registry" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Options struct { | type Options struct { | ||||||
| 	Namespace string |  | ||||||
| 	Handler  string | 	Handler  string | ||||||
| 	Registry registry.Registry | 	Registry registry.Registry | ||||||
| 	Resolver resolver.Resolver | 	Resolver resolver.Resolver | ||||||
| @@ -19,7 +17,7 @@ type Option func(o *Options) | |||||||
| func NewOptions(opts ...Option) Options { | func NewOptions(opts ...Option) Options { | ||||||
| 	options := Options{ | 	options := Options{ | ||||||
| 		Handler:  "meta", | 		Handler:  "meta", | ||||||
| 		Registry: *cmd.DefaultOptions().Registry, | 		Registry: registry.DefaultRegistry, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, o := range opts { | 	for _, o := range opts { | ||||||
| @@ -27,9 +25,8 @@ func NewOptions(opts ...Option) Options { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if options.Resolver == nil { | 	if options.Resolver == nil { | ||||||
| 		options.Resolver = micro.NewResolver( | 		options.Resolver = vpath.NewResolver( | ||||||
| 			resolver.WithHandler(options.Handler), | 			resolver.WithHandler(options.Handler), | ||||||
| 			resolver.WithNamespace(options.Namespace), |  | ||||||
| 		) | 		) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -42,12 +39,6 @@ func WithHandler(h string) Option { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func WithNamespace(ns string) Option { |  | ||||||
| 	return func(o *Options) { |  | ||||||
| 		o.Namespace = ns |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func WithRegistry(r registry.Registry) Option { | func WithRegistry(r registry.Registry) Option { | ||||||
| 	return func(o *Options) { | 	return func(o *Options) { | ||||||
| 		o.Registry = r | 		o.Registry = r | ||||||
|   | |||||||
| @@ -4,19 +4,28 @@ package registry | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"log" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api" | 	"github.com/micro/go-micro/v2/api" | ||||||
| 	"github.com/micro/go-micro/api/router" | 	"github.com/micro/go-micro/v2/api/router" | ||||||
| 	"github.com/micro/go-micro/registry" | 	"github.com/micro/go-micro/v2/api/router/util" | ||||||
| 	"github.com/micro/go-micro/registry/cache" | 	"github.com/micro/go-micro/v2/logger" | ||||||
|  | 	"github.com/micro/go-micro/v2/metadata" | ||||||
|  | 	"github.com/micro/go-micro/v2/registry" | ||||||
|  | 	"github.com/micro/go-micro/v2/registry/cache" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // endpoint struct, that holds compiled pcre | ||||||
|  | type endpoint struct { | ||||||
|  | 	hostregs []*regexp.Regexp | ||||||
|  | 	pathregs []util.Pattern | ||||||
|  | 	pcreregs []*regexp.Regexp | ||||||
|  | } | ||||||
|  |  | ||||||
| // router is the default router | // router is the default router | ||||||
| type registryRouter struct { | type registryRouter struct { | ||||||
| 	exit chan bool | 	exit chan bool | ||||||
| @@ -27,28 +36,8 @@ type registryRouter struct { | |||||||
|  |  | ||||||
| 	sync.RWMutex | 	sync.RWMutex | ||||||
| 	eps map[string]*api.Service | 	eps map[string]*api.Service | ||||||
| } | 	// compiled regexp for host and path | ||||||
|  | 	ceps map[string]*endpoint | ||||||
| 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 { | func (r *registryRouter) isClosed() bool { | ||||||
| @@ -68,7 +57,9 @@ func (r *registryRouter) refresh() { | |||||||
| 		services, err := r.opts.Registry.ListServices() | 		services, err := r.opts.Registry.ListServices() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			attempts++ | 			attempts++ | ||||||
| 			log.Println("Error listing endpoints", err) | 			if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 				logger.Errorf("unable to list services: %v", err) | ||||||
|  | 			} | ||||||
| 			time.Sleep(time.Duration(attempts) * time.Second) | 			time.Sleep(time.Duration(attempts) * time.Second) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -77,18 +68,18 @@ func (r *registryRouter) refresh() { | |||||||
|  |  | ||||||
| 		// for each service, get service and store endpoints | 		// for each service, get service and store endpoints | ||||||
| 		for _, s := range services { | 		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) | 			service, err := r.rc.GetService(s.Name) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | 				if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Errorf("unable to get service: %v", err) | ||||||
|  | 				} | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			r.store(service) | 			r.store(service) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// refresh list in 10 minutes... cruft | 		// refresh list in 10 minutes... cruft | ||||||
|  | 		// use registry watching | ||||||
| 		select { | 		select { | ||||||
| 		case <-time.After(time.Minute * 10): | 		case <-time.After(time.Minute * 10): | ||||||
| 		case <-r.exit: | 		case <-r.exit: | ||||||
| @@ -100,13 +91,16 @@ func (r *registryRouter) refresh() { | |||||||
| // process watch event | // process watch event | ||||||
| func (r *registryRouter) process(res *registry.Result) { | func (r *registryRouter) process(res *registry.Result) { | ||||||
| 	// skip these things | 	// skip these things | ||||||
| 	if res == nil || res.Service == nil || !strings.HasPrefix(res.Service.Name, r.opts.Namespace) { | 	if res == nil || res.Service == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// get entry from cache | 	// get entry from cache | ||||||
| 	service, err := r.rc.GetService(res.Service.Name) | 	service, err := r.rc.GetService(res.Service.Name) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | 		if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Errorf("unable to get service: %v", err) | ||||||
|  | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -128,14 +122,17 @@ func (r *registryRouter) store(services []*registry.Service) { | |||||||
| 		names[service.Name] = true | 		names[service.Name] = true | ||||||
|  |  | ||||||
| 		// map per endpoint | 		// map per endpoint | ||||||
| 		for _, endpoint := range service.Endpoints { | 		for _, sep := range service.Endpoints { | ||||||
| 			// create a key service:endpoint_name | 			// create a key service:endpoint_name | ||||||
| 			key := fmt.Sprintf("%s:%s", service.Name, endpoint.Name) | 			key := fmt.Sprintf("%s.%s", service.Name, sep.Name) | ||||||
| 			// decode endpoint | 			// decode endpoint | ||||||
| 			end := api.Decode(endpoint.Metadata) | 			end := api.Decode(sep.Metadata) | ||||||
|  |  | ||||||
| 			// if we got nothing skip | 			// if we got nothing skip | ||||||
| 			if err := api.Validate(end); err != nil { | 			if err := api.Validate(end); err != nil { | ||||||
|  | 				if logger.V(logger.TraceLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Tracef("endpoint validation failed: %v", err) | ||||||
|  | 				} | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -170,8 +167,57 @@ func (r *registryRouter) store(services []*registry.Service) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// now set the eps we have | 	// now set the eps we have | ||||||
| 	for name, endpoint := range eps { | 	for name, ep := range eps { | ||||||
| 		r.eps[name] = endpoint | 		r.eps[name] = ep | ||||||
|  | 		cep := &endpoint{} | ||||||
|  |  | ||||||
|  | 		for _, h := range ep.Endpoint.Host { | ||||||
|  | 			if h == "" || h == "*" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			hostreg, err := regexp.CompilePOSIX(h) | ||||||
|  | 			if err != nil { | ||||||
|  | 				if logger.V(logger.TraceLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Tracef("endpoint have invalid host regexp: %v", err) | ||||||
|  | 				} | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			cep.hostregs = append(cep.hostregs, hostreg) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, p := range ep.Endpoint.Path { | ||||||
|  | 			var pcreok bool | ||||||
|  |  | ||||||
|  | 			if p[0] == '^' && p[len(p)-1] != '$' { | ||||||
|  | 				pcrereg, err := regexp.CompilePOSIX(p) | ||||||
|  | 				if err == nil { | ||||||
|  | 					cep.pcreregs = append(cep.pcreregs, pcrereg) | ||||||
|  | 					pcreok = true | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			rule, err := util.Parse(p) | ||||||
|  | 			if err != nil && !pcreok { | ||||||
|  | 				if logger.V(logger.TraceLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Tracef("endpoint have invalid path pattern: %v", err) | ||||||
|  | 				} | ||||||
|  | 				continue | ||||||
|  | 			} else if err != nil && pcreok { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			tpl := rule.Compile() | ||||||
|  | 			pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "") | ||||||
|  | 			if err != nil { | ||||||
|  | 				if logger.V(logger.TraceLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Tracef("endpoint have invalid path pattern: %v", err) | ||||||
|  | 				} | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			cep.pathregs = append(cep.pathregs, pathreg) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		r.ceps[name] = cep | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -188,7 +234,9 @@ func (r *registryRouter) watch() { | |||||||
| 		w, err := r.opts.Registry.Watch() | 		w, err := r.opts.Registry.Watch() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			attempts++ | 			attempts++ | ||||||
| 			log.Println("Error watching endpoints", err) | 			if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 				logger.Errorf("error watching endpoints: %v", err) | ||||||
|  | 			} | ||||||
| 			time.Sleep(time.Duration(attempts) * time.Second) | 			time.Sleep(time.Duration(attempts) * time.Second) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -211,7 +259,9 @@ func (r *registryRouter) watch() { | |||||||
| 			// process next event | 			// process next event | ||||||
| 			res, err := w.Next() | 			res, err := w.Next() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Println("Error getting next endpoint", err) | 				if logger.V(logger.ErrorLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Errorf("error getting next endoint: %v", err) | ||||||
|  | 				} | ||||||
| 				close(ch) | 				close(ch) | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| @@ -235,6 +285,14 @@ func (r *registryRouter) Close() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (r *registryRouter) Register(ep *api.Endpoint) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *registryRouter) Deregister(ep *api.Endpoint) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) { | func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) { | ||||||
| 	if r.isClosed() { | 	if r.isClosed() { | ||||||
| 		return nil, errors.New("router closed") | 		return nil, errors.New("router closed") | ||||||
| @@ -243,60 +301,106 @@ func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) { | |||||||
| 	r.RLock() | 	r.RLock() | ||||||
| 	defer r.RUnlock() | 	defer r.RUnlock() | ||||||
|  |  | ||||||
|  | 	var idx int | ||||||
|  | 	if len(req.URL.Path) > 0 && req.URL.Path != "/" { | ||||||
|  | 		idx = 1 | ||||||
|  | 	} | ||||||
|  | 	path := strings.Split(req.URL.Path[idx:], "/") | ||||||
|  |  | ||||||
| 	// use the first match | 	// use the first match | ||||||
| 	// TODO: weighted matching | 	// TODO: weighted matching | ||||||
| 	for _, e := range r.eps { | 	for n, e := range r.eps { | ||||||
|  | 		cep, ok := r.ceps[n] | ||||||
|  | 		if !ok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
| 		ep := e.Endpoint | 		ep := e.Endpoint | ||||||
|  | 		var mMatch, hMatch, pMatch bool | ||||||
| 		// match | 		// 1. try method | ||||||
| 		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 { | 		for _, m := range ep.Method { | ||||||
| 			if req.Method == m { | 			if m == req.Method { | ||||||
| 				methodMatch = true | 				mMatch = true | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		if !mMatch { | ||||||
| 		// no match on method pass |  | ||||||
| 		if len(ep.Method) > 0 && !methodMatch { |  | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Debugf("api method match %s", req.Method) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// 2. try match host | 		// 2. try host | ||||||
| 		for _, h := range ep.Host { | 		if len(ep.Host) == 0 { | ||||||
| 			if req.Host == h { | 			hMatch = true | ||||||
| 				hostMatch = true | 		} else { | ||||||
|  | 			for idx, h := range ep.Host { | ||||||
|  | 				if h == "" || h == "*" { | ||||||
|  | 					hMatch = true | ||||||
|  | 					break | ||||||
|  | 				} else { | ||||||
|  | 					if cep.hostregs[idx].MatchString(req.URL.Host) { | ||||||
|  | 						hMatch = true | ||||||
| 						break | 						break | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  | 			} | ||||||
| 		// no match on host pass | 		} | ||||||
| 		if len(ep.Host) > 0 && !hostMatch { | 		if !hMatch { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Debugf("api host match %s", req.URL.Host) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// 3. try match paths | 		// 3. try path via google.api path matching | ||||||
| 		for _, p := range ep.Path { | 		for _, pathreg := range cep.pathregs { | ||||||
| 			re, err := regexp.CompilePOSIX(p) | 			matches, err := pathreg.Match(path, "") | ||||||
| 			if err == nil && re.MatchString(req.URL.Path) { | 			if err != nil { | ||||||
| 				pathMatch = true | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debugf("api gpath not match %s != %v", path, pathreg) | ||||||
|  | 				} | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 				logger.Debugf("api gpath match %s = %v", path, pathreg) | ||||||
|  | 			} | ||||||
|  | 			pMatch = true | ||||||
|  | 			ctx := req.Context() | ||||||
|  | 			md, ok := metadata.FromContext(ctx) | ||||||
|  | 			if !ok { | ||||||
|  | 				md = make(metadata.Metadata) | ||||||
|  | 			} | ||||||
|  | 			for k, v := range matches { | ||||||
|  | 				md[fmt.Sprintf("x-api-field-%s", k)] = v | ||||||
|  | 			} | ||||||
|  | 			md["x-api-body"] = ep.Body | ||||||
|  | 			*req = *req.Clone(metadata.NewContext(ctx, md)) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !pMatch { | ||||||
|  | 			// 4. try path via pcre path matching | ||||||
|  | 			for _, pathreg := range cep.pcreregs { | ||||||
|  | 				if !pathreg.MatchString(req.URL.Path) { | ||||||
|  | 					if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 						logger.Debugf("api pcre path not match %s != %v", path, pathreg) | ||||||
|  | 					} | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debugf("api pcre path match %s != %v", path, pathreg) | ||||||
|  | 				} | ||||||
|  | 				pMatch = true | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// no match pass | 		if !pMatch { | ||||||
| 		if len(ep.Path) > 0 && !pathMatch { |  | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// TODO: Percentage traffic | 		// TODO: Percentage traffic | ||||||
|  |  | ||||||
| 		// we got here, so its a match | 		// we got here, so its a match | ||||||
| 		return e, nil | 		return e, nil | ||||||
| 	} | 	} | ||||||
| @@ -327,7 +431,7 @@ func (r *registryRouter) Route(req *http.Request) (*api.Service, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// service name | 	// service name | ||||||
| 	name := setNamespace(r.opts.Namespace, rp.Name) | 	name := rp.Name | ||||||
|  |  | ||||||
| 	// get service | 	// get service | ||||||
| 	services, err := r.rc.GetService(name) | 	services, err := r.rc.GetService(name) | ||||||
| @@ -381,6 +485,7 @@ func newRouter(opts ...router.Option) *registryRouter { | |||||||
| 		opts: options, | 		opts: options, | ||||||
| 		rc:   cache.New(options.Registry), | 		rc:   cache.New(options.Registry), | ||||||
| 		eps:  make(map[string]*api.Service), | 		eps:  make(map[string]*api.Service), | ||||||
|  | 		ceps: make(map[string]*endpoint), | ||||||
| 	} | 	} | ||||||
| 	go r.watch() | 	go r.watch() | ||||||
| 	go r.refresh() | 	go r.refresh() | ||||||
|   | |||||||
| @@ -1,181 +0,0 @@ | |||||||
| 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) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -4,7 +4,7 @@ package router | |||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api" | 	"github.com/micro/go-micro/v2/api" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Router is used to determine an endpoint for a request | // Router is used to determine an endpoint for a request | ||||||
| @@ -15,6 +15,10 @@ type Router interface { | |||||||
| 	Close() error | 	Close() error | ||||||
| 	// Endpoint returns an api.Service endpoint or an error if it does not exist | 	// Endpoint returns an api.Service endpoint or an error if it does not exist | ||||||
| 	Endpoint(r *http.Request) (*api.Service, error) | 	Endpoint(r *http.Request) (*api.Service, error) | ||||||
|  | 	// Register endpoint in router | ||||||
|  | 	Register(ep *api.Endpoint) error | ||||||
|  | 	// Deregister endpoint from router | ||||||
|  | 	Deregister(ep *api.Endpoint) error | ||||||
| 	// Route returns an api.Service route | 	// Route returns an api.Service route | ||||||
| 	Route(r *http.Request) (*api.Service, error) | 	Route(r *http.Request) (*api.Service, error) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										245
									
								
								api/router/router_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								api/router/router_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | |||||||
|  | package router_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/api" | ||||||
|  | 	"github.com/micro/go-micro/v2/api/handler" | ||||||
|  | 	"github.com/micro/go-micro/v2/api/handler/rpc" | ||||||
|  | 	"github.com/micro/go-micro/v2/api/router" | ||||||
|  | 	rregistry "github.com/micro/go-micro/v2/api/router/registry" | ||||||
|  | 	rstatic "github.com/micro/go-micro/v2/api/router/static" | ||||||
|  | 	"github.com/micro/go-micro/v2/client" | ||||||
|  | 	gcli "github.com/micro/go-micro/v2/client/grpc" | ||||||
|  | 	rmemory "github.com/micro/go-micro/v2/registry/memory" | ||||||
|  | 	"github.com/micro/go-micro/v2/server" | ||||||
|  | 	gsrv "github.com/micro/go-micro/v2/server/grpc" | ||||||
|  | 	pb "github.com/micro/go-micro/v2/server/grpc/proto" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // server is used to implement helloworld.GreeterServer. | ||||||
|  | type testServer struct { | ||||||
|  | 	msgCount int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestHello implements helloworld.GreeterServer | ||||||
|  | func (s *testServer) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error { | ||||||
|  | 	rsp.Msg = "Hello " + req.Uuid | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestHello implements helloworld.GreeterServer | ||||||
|  | func (s *testServer) CallPcre(ctx context.Context, req *pb.Request, rsp *pb.Response) error { | ||||||
|  | 	rsp.Msg = "Hello " + req.Uuid | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestHello implements helloworld.GreeterServer | ||||||
|  | func (s *testServer) CallPcreInvalid(ctx context.Context, req *pb.Request, rsp *pb.Response) error { | ||||||
|  | 	rsp.Msg = "Hello " + req.Uuid | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func initial(t *testing.T) (server.Server, client.Client) { | ||||||
|  | 	r := rmemory.NewRegistry() | ||||||
|  |  | ||||||
|  | 	// create a new client | ||||||
|  | 	s := gsrv.NewServer( | ||||||
|  | 		server.Name("foo"), | ||||||
|  | 		server.Registry(r), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	// create a new server | ||||||
|  | 	c := gcli.NewClient( | ||||||
|  | 		client.Registry(r), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	h := &testServer{} | ||||||
|  | 	pb.RegisterTestHandler(s, h) | ||||||
|  |  | ||||||
|  | 	if err := s.Start(); err != nil { | ||||||
|  | 		t.Fatalf("failed to start: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return s, c | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func check(t *testing.T, addr string, path string, expected string) { | ||||||
|  | 	req, err := http.NewRequest("POST", fmt.Sprintf(path, addr), nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to created http.Request: %v", err) | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Content-Type", "application/json") | ||||||
|  | 	rsp, err := (&http.Client{}).Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to created http.Request: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer rsp.Body.Close() | ||||||
|  |  | ||||||
|  | 	buf, err := ioutil.ReadAll(rsp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	jsonMsg := expected | ||||||
|  | 	if string(buf) != jsonMsg { | ||||||
|  | 		t.Fatalf("invalid message received, parsing error %s != %s", buf, jsonMsg) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRouterRegistryPcre(t *testing.T) { | ||||||
|  | 	s, c := initial(t) | ||||||
|  | 	defer s.Stop() | ||||||
|  |  | ||||||
|  | 	router := rregistry.NewRouter( | ||||||
|  | 		router.WithHandler(rpc.Handler), | ||||||
|  | 		router.WithRegistry(s.Options().Registry), | ||||||
|  | 	) | ||||||
|  | 	hrpc := rpc.NewHandler( | ||||||
|  | 		handler.WithClient(c), | ||||||
|  | 		handler.WithRouter(router), | ||||||
|  | 	) | ||||||
|  | 	hsrv := &http.Server{ | ||||||
|  | 		Handler:        hrpc, | ||||||
|  | 		Addr:           "127.0.0.1:6543", | ||||||
|  | 		WriteTimeout:   15 * time.Second, | ||||||
|  | 		ReadTimeout:    15 * time.Second, | ||||||
|  | 		IdleTimeout:    20 * time.Second, | ||||||
|  | 		MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		log.Println(hsrv.ListenAndServe()) | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	defer hsrv.Close() | ||||||
|  | 	time.Sleep(1 * time.Second) | ||||||
|  | 	check(t, hsrv.Addr, "http://%s/api/v0/test/call/TEST", `{"msg":"Hello TEST"}`) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRouterStaticPcre(t *testing.T) { | ||||||
|  | 	s, c := initial(t) | ||||||
|  | 	defer s.Stop() | ||||||
|  |  | ||||||
|  | 	router := rstatic.NewRouter( | ||||||
|  | 		router.WithHandler(rpc.Handler), | ||||||
|  | 		router.WithRegistry(s.Options().Registry), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	err := router.Register(&api.Endpoint{ | ||||||
|  | 		Name:    "foo.Test.Call", | ||||||
|  | 		Method:  []string{"POST"}, | ||||||
|  | 		Path:    []string{"^/api/v0/test/call/?$"}, | ||||||
|  | 		Handler: "rpc", | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hrpc := rpc.NewHandler( | ||||||
|  | 		handler.WithClient(c), | ||||||
|  | 		handler.WithRouter(router), | ||||||
|  | 	) | ||||||
|  | 	hsrv := &http.Server{ | ||||||
|  | 		Handler:        hrpc, | ||||||
|  | 		Addr:           "127.0.0.1:6543", | ||||||
|  | 		WriteTimeout:   15 * time.Second, | ||||||
|  | 		ReadTimeout:    15 * time.Second, | ||||||
|  | 		IdleTimeout:    20 * time.Second, | ||||||
|  | 		MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		log.Println(hsrv.ListenAndServe()) | ||||||
|  | 	}() | ||||||
|  | 	defer hsrv.Close() | ||||||
|  |  | ||||||
|  | 	time.Sleep(1 * time.Second) | ||||||
|  | 	check(t, hsrv.Addr, "http://%s/api/v0/test/call", `{"msg":"Hello "}`) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRouterStaticGpath(t *testing.T) { | ||||||
|  | 	s, c := initial(t) | ||||||
|  | 	defer s.Stop() | ||||||
|  |  | ||||||
|  | 	router := rstatic.NewRouter( | ||||||
|  | 		router.WithHandler(rpc.Handler), | ||||||
|  | 		router.WithRegistry(s.Options().Registry), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	err := router.Register(&api.Endpoint{ | ||||||
|  | 		Name:    "foo.Test.Call", | ||||||
|  | 		Method:  []string{"POST"}, | ||||||
|  | 		Path:    []string{"/api/v0/test/call/{uuid}"}, | ||||||
|  | 		Handler: "rpc", | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hrpc := rpc.NewHandler( | ||||||
|  | 		handler.WithClient(c), | ||||||
|  | 		handler.WithRouter(router), | ||||||
|  | 	) | ||||||
|  | 	hsrv := &http.Server{ | ||||||
|  | 		Handler:        hrpc, | ||||||
|  | 		Addr:           "127.0.0.1:6543", | ||||||
|  | 		WriteTimeout:   15 * time.Second, | ||||||
|  | 		ReadTimeout:    15 * time.Second, | ||||||
|  | 		IdleTimeout:    20 * time.Second, | ||||||
|  | 		MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		log.Println(hsrv.ListenAndServe()) | ||||||
|  | 	}() | ||||||
|  | 	defer hsrv.Close() | ||||||
|  |  | ||||||
|  | 	time.Sleep(1 * time.Second) | ||||||
|  | 	check(t, hsrv.Addr, "http://%s/api/v0/test/call/TEST", `{"msg":"Hello TEST"}`) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRouterStaticPcreInvalid(t *testing.T) { | ||||||
|  | 	var ep *api.Endpoint | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
|  | 	s, c := initial(t) | ||||||
|  | 	defer s.Stop() | ||||||
|  |  | ||||||
|  | 	router := rstatic.NewRouter( | ||||||
|  | 		router.WithHandler(rpc.Handler), | ||||||
|  | 		router.WithRegistry(s.Options().Registry), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	ep = &api.Endpoint{ | ||||||
|  | 		Name:    "foo.Test.Call", | ||||||
|  | 		Method:  []string{"POST"}, | ||||||
|  | 		Path:    []string{"^/api/v0/test/call/?"}, | ||||||
|  | 		Handler: "rpc", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = router.Register(ep) | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("invalid endpoint %v", ep) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ep = &api.Endpoint{ | ||||||
|  | 		Name:    "foo.Test.Call", | ||||||
|  | 		Method:  []string{"POST"}, | ||||||
|  | 		Path:    []string{"/api/v0/test/call/?$"}, | ||||||
|  | 		Handler: "rpc", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = router.Register(ep) | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("invalid endpoint %v", ep) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_ = c | ||||||
|  | } | ||||||
							
								
								
									
										356
									
								
								api/router/static/static.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								api/router/static/static.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,356 @@ | |||||||
|  | package static | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/api" | ||||||
|  | 	"github.com/micro/go-micro/v2/api/router" | ||||||
|  | 	"github.com/micro/go-micro/v2/api/router/util" | ||||||
|  | 	"github.com/micro/go-micro/v2/logger" | ||||||
|  | 	"github.com/micro/go-micro/v2/metadata" | ||||||
|  | 	"github.com/micro/go-micro/v2/registry" | ||||||
|  | 	rutil "github.com/micro/go-micro/v2/util/registry" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type endpoint struct { | ||||||
|  | 	apiep    *api.Endpoint | ||||||
|  | 	hostregs []*regexp.Regexp | ||||||
|  | 	pathregs []util.Pattern | ||||||
|  | 	pcreregs []*regexp.Regexp | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // router is the default router | ||||||
|  | type staticRouter struct { | ||||||
|  | 	exit chan bool | ||||||
|  | 	opts router.Options | ||||||
|  | 	sync.RWMutex | ||||||
|  | 	eps map[string]*endpoint | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *staticRouter) isClosed() bool { | ||||||
|  | 	select { | ||||||
|  | 	case <-r.exit: | ||||||
|  | 		return true | ||||||
|  | 	default: | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | // watch for endpoint changes | ||||||
|  | func (r *staticRouter) 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 *staticRouter) Register(ep *api.Endpoint) error { | ||||||
|  | 	if err := api.Validate(ep); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var pathregs []util.Pattern | ||||||
|  | 	var hostregs []*regexp.Regexp | ||||||
|  | 	var pcreregs []*regexp.Regexp | ||||||
|  |  | ||||||
|  | 	for _, h := range ep.Host { | ||||||
|  | 		if h == "" || h == "*" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		hostreg, err := regexp.CompilePOSIX(h) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		hostregs = append(hostregs, hostreg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, p := range ep.Path { | ||||||
|  | 		var pcreok bool | ||||||
|  |  | ||||||
|  | 		// pcre only when we have start and end markers | ||||||
|  | 		if p[0] == '^' && p[len(p)-1] == '$' { | ||||||
|  | 			pcrereg, err := regexp.CompilePOSIX(p) | ||||||
|  | 			if err == nil { | ||||||
|  | 				pcreregs = append(pcreregs, pcrereg) | ||||||
|  | 				pcreok = true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		rule, err := util.Parse(p) | ||||||
|  | 		if err != nil && !pcreok { | ||||||
|  | 			return err | ||||||
|  | 		} else if err != nil && pcreok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		tpl := rule.Compile() | ||||||
|  | 		pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "") | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		pathregs = append(pathregs, pathreg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.Lock() | ||||||
|  | 	r.eps[ep.Name] = &endpoint{ | ||||||
|  | 		apiep:    ep, | ||||||
|  | 		pcreregs: pcreregs, | ||||||
|  | 		pathregs: pathregs, | ||||||
|  | 		hostregs: hostregs, | ||||||
|  | 	} | ||||||
|  | 	r.Unlock() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *staticRouter) Deregister(ep *api.Endpoint) error { | ||||||
|  | 	if err := api.Validate(ep); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	r.Lock() | ||||||
|  | 	delete(r.eps, ep.Name) | ||||||
|  | 	r.Unlock() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *staticRouter) Options() router.Options { | ||||||
|  | 	return r.opts | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *staticRouter) Close() error { | ||||||
|  | 	select { | ||||||
|  | 	case <-r.exit: | ||||||
|  | 		return nil | ||||||
|  | 	default: | ||||||
|  | 		close(r.exit) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *staticRouter) Endpoint(req *http.Request) (*api.Service, error) { | ||||||
|  | 	ep, err := r.endpoint(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	epf := strings.Split(ep.apiep.Name, ".") | ||||||
|  | 	services, err := r.opts.Registry.GetService(epf[0]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// hack for stream endpoint | ||||||
|  | 	if ep.apiep.Stream { | ||||||
|  | 		svcs := rutil.Copy(services) | ||||||
|  | 		for _, svc := range svcs { | ||||||
|  | 			if len(svc.Endpoints) == 0 { | ||||||
|  | 				e := ®istry.Endpoint{} | ||||||
|  | 				e.Name = strings.Join(epf[1:], ".") | ||||||
|  | 				e.Metadata = make(map[string]string) | ||||||
|  | 				e.Metadata["stream"] = "true" | ||||||
|  | 				svc.Endpoints = append(svc.Endpoints, e) | ||||||
|  | 			} | ||||||
|  | 			for _, e := range svc.Endpoints { | ||||||
|  | 				e.Name = strings.Join(epf[1:], ".") | ||||||
|  | 				e.Metadata = make(map[string]string) | ||||||
|  | 				e.Metadata["stream"] = "true" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		services = svcs | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	svc := &api.Service{ | ||||||
|  | 		Name: epf[0], | ||||||
|  | 		Endpoint: &api.Endpoint{ | ||||||
|  | 			Name:    strings.Join(epf[1:], "."), | ||||||
|  | 			Handler: "rpc", | ||||||
|  | 			Host:    ep.apiep.Host, | ||||||
|  | 			Method:  ep.apiep.Method, | ||||||
|  | 			Path:    ep.apiep.Path, | ||||||
|  | 			Body:    ep.apiep.Body, | ||||||
|  | 			Stream:  ep.apiep.Stream, | ||||||
|  | 		}, | ||||||
|  | 		Services: services, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return svc, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *staticRouter) endpoint(req *http.Request) (*endpoint, error) { | ||||||
|  | 	if r.isClosed() { | ||||||
|  | 		return nil, errors.New("router closed") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.RLock() | ||||||
|  | 	defer r.RUnlock() | ||||||
|  |  | ||||||
|  | 	var idx int | ||||||
|  | 	if len(req.URL.Path) > 0 && req.URL.Path != "/" { | ||||||
|  | 		idx = 1 | ||||||
|  | 	} | ||||||
|  | 	path := strings.Split(req.URL.Path[idx:], "/") | ||||||
|  | 	// use the first match | ||||||
|  | 	// TODO: weighted matching | ||||||
|  |  | ||||||
|  | 	for _, ep := range r.eps { | ||||||
|  | 		var mMatch, hMatch, pMatch bool | ||||||
|  |  | ||||||
|  | 		// 1. try method | ||||||
|  | 		for _, m := range ep.apiep.Method { | ||||||
|  | 			if m == req.Method { | ||||||
|  | 				mMatch = true | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if !mMatch { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Debugf("api method match %s", req.Method) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 2. try host | ||||||
|  | 		if len(ep.apiep.Host) == 0 { | ||||||
|  | 			hMatch = true | ||||||
|  | 		} else { | ||||||
|  | 			for idx, h := range ep.apiep.Host { | ||||||
|  | 				if h == "" || h == "*" { | ||||||
|  | 					hMatch = true | ||||||
|  | 					break | ||||||
|  | 				} else { | ||||||
|  | 					if ep.hostregs[idx].MatchString(req.URL.Host) { | ||||||
|  | 						hMatch = true | ||||||
|  | 						break | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if !hMatch { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Debugf("api host match %s", req.URL.Host) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 3. try google.api path | ||||||
|  | 		for _, pathreg := range ep.pathregs { | ||||||
|  | 			matches, err := pathreg.Match(path, "") | ||||||
|  | 			if err != nil { | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debugf("api gpath not match %s != %v", path, pathreg) | ||||||
|  | 				} | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 				logger.Debugf("api gpath match %s = %v", path, pathreg) | ||||||
|  | 			} | ||||||
|  | 			pMatch = true | ||||||
|  | 			ctx := req.Context() | ||||||
|  | 			md, ok := metadata.FromContext(ctx) | ||||||
|  | 			if !ok { | ||||||
|  | 				md = make(metadata.Metadata) | ||||||
|  | 			} | ||||||
|  | 			for k, v := range matches { | ||||||
|  | 				md[fmt.Sprintf("x-api-field-%s", k)] = v | ||||||
|  | 			} | ||||||
|  | 			md["x-api-body"] = ep.apiep.Body | ||||||
|  | 			*req = *req.Clone(metadata.NewContext(ctx, md)) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !pMatch { | ||||||
|  | 			// 4. try path via pcre path matching | ||||||
|  | 			for _, pathreg := range ep.pcreregs { | ||||||
|  | 				if !pathreg.MatchString(req.URL.Path) { | ||||||
|  | 					if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 						logger.Debugf("api pcre path not match %s != %v", req.URL.Path, pathreg) | ||||||
|  | 					} | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				pMatch = true | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !pMatch { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		// TODO: Percentage traffic | ||||||
|  |  | ||||||
|  | 		// we got here, so its a match | ||||||
|  | 		return ep, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// no match | ||||||
|  | 	return nil, fmt.Errorf("endpoint not found for %v", req.URL) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *staticRouter) 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 nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ep, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewRouter(opts ...router.Option) *staticRouter { | ||||||
|  | 	options := router.NewOptions(opts...) | ||||||
|  | 	r := &staticRouter{ | ||||||
|  | 		exit: make(chan bool), | ||||||
|  | 		opts: options, | ||||||
|  | 		eps:  make(map[string]*endpoint), | ||||||
|  | 	} | ||||||
|  | 	//go r.watch() | ||||||
|  | 	//go r.refresh() | ||||||
|  | 	return r | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								api/router/util/LICENSE.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/router/util/LICENSE.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | Copyright (c) 2015, Gengo, Inc. | ||||||
|  | All rights reserved. | ||||||
|  |  | ||||||
|  | Redistribution and use in source and binary forms, with or without modification, | ||||||
|  | are permitted provided that the following conditions are met: | ||||||
|  |  | ||||||
|  |     * Redistributions of source code must retain the above copyright notice, | ||||||
|  |       this list of conditions and the following disclaimer. | ||||||
|  |  | ||||||
|  |     * Redistributions in binary form must reproduce the above copyright notice, | ||||||
|  |       this list of conditions and the following disclaimer in the documentation | ||||||
|  |       and/or other materials provided with the distribution. | ||||||
|  |  | ||||||
|  |     * Neither the name of Gengo, Inc. nor the names of its | ||||||
|  |       contributors may be used to endorse or promote products derived from this | ||||||
|  |       software without specific prior written permission. | ||||||
|  |  | ||||||
|  | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||||||
|  | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||||||
|  | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||||
|  | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||||||
|  | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||||
|  | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||||
|  | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||||||
|  | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||
|  | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||||
|  | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||||
							
								
								
									
										115
									
								
								api/router/util/compile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								api/router/util/compile.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile.go | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	opcodeVersion = 1 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Template is a compiled representation of path templates. | ||||||
|  | type Template struct { | ||||||
|  | 	// Version is the version number of the format. | ||||||
|  | 	Version int | ||||||
|  | 	// OpCodes is a sequence of operations. | ||||||
|  | 	OpCodes []int | ||||||
|  | 	// Pool is a constant pool | ||||||
|  | 	Pool []string | ||||||
|  | 	// Verb is a VERB part in the template. | ||||||
|  | 	Verb string | ||||||
|  | 	// Fields is a list of field paths bound in this template. | ||||||
|  | 	Fields []string | ||||||
|  | 	// Original template (example: /v1/a_bit_of_everything) | ||||||
|  | 	Template string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Compiler compiles utilities representation of path templates into marshallable operations. | ||||||
|  | // They can be unmarshalled by runtime.NewPattern. | ||||||
|  | type Compiler interface { | ||||||
|  | 	Compile() Template | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type op struct { | ||||||
|  | 	// code is the opcode of the operation | ||||||
|  | 	code OpCode | ||||||
|  |  | ||||||
|  | 	// str is a string operand of the code. | ||||||
|  | 	// operand is ignored if str is not empty. | ||||||
|  | 	str string | ||||||
|  |  | ||||||
|  | 	// operand is a numeric operand of the code. | ||||||
|  | 	operand int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w wildcard) compile() []op { | ||||||
|  | 	return []op{ | ||||||
|  | 		{code: OpPush}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w deepWildcard) compile() []op { | ||||||
|  | 	return []op{ | ||||||
|  | 		{code: OpPushM}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l literal) compile() []op { | ||||||
|  | 	return []op{ | ||||||
|  | 		{ | ||||||
|  | 			code: OpLitPush, | ||||||
|  | 			str:  string(l), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (v variable) compile() []op { | ||||||
|  | 	var ops []op | ||||||
|  | 	for _, s := range v.segments { | ||||||
|  | 		ops = append(ops, s.compile()...) | ||||||
|  | 	} | ||||||
|  | 	ops = append(ops, op{ | ||||||
|  | 		code:    OpConcatN, | ||||||
|  | 		operand: len(v.segments), | ||||||
|  | 	}, op{ | ||||||
|  | 		code: OpCapture, | ||||||
|  | 		str:  v.path, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return ops | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t template) Compile() Template { | ||||||
|  | 	var rawOps []op | ||||||
|  | 	for _, s := range t.segments { | ||||||
|  | 		rawOps = append(rawOps, s.compile()...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		ops    []int | ||||||
|  | 		pool   []string | ||||||
|  | 		fields []string | ||||||
|  | 	) | ||||||
|  | 	consts := make(map[string]int) | ||||||
|  | 	for _, op := range rawOps { | ||||||
|  | 		ops = append(ops, int(op.code)) | ||||||
|  | 		if op.str == "" { | ||||||
|  | 			ops = append(ops, op.operand) | ||||||
|  | 		} else { | ||||||
|  | 			if _, ok := consts[op.str]; !ok { | ||||||
|  | 				consts[op.str] = len(pool) | ||||||
|  | 				pool = append(pool, op.str) | ||||||
|  | 			} | ||||||
|  | 			ops = append(ops, consts[op.str]) | ||||||
|  | 		} | ||||||
|  | 		if op.code == OpCapture { | ||||||
|  | 			fields = append(fields, op.str) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return Template{ | ||||||
|  | 		Version:  opcodeVersion, | ||||||
|  | 		OpCodes:  ops, | ||||||
|  | 		Pool:     pool, | ||||||
|  | 		Verb:     t.verb, | ||||||
|  | 		Fields:   fields, | ||||||
|  | 		Template: t.template, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										122
									
								
								api/router/util/compile_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								api/router/util/compile_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile_test.go | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	operandFiller = 0 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestCompile(t *testing.T) { | ||||||
|  | 	for _, spec := range []struct { | ||||||
|  | 		segs []segment | ||||||
|  | 		verb string | ||||||
|  |  | ||||||
|  | 		ops    []int | ||||||
|  | 		pool   []string | ||||||
|  | 		fields []string | ||||||
|  | 	}{ | ||||||
|  | 		{}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				wildcard{}, | ||||||
|  | 			}, | ||||||
|  | 			ops: []int{int(OpPush), operandFiller}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				deepWildcard{}, | ||||||
|  | 			}, | ||||||
|  | 			ops: []int{int(OpPushM), operandFiller}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 			}, | ||||||
|  | 			ops:  []int{int(OpLitPush), 0}, | ||||||
|  | 			pool: []string{"v1"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 			}, | ||||||
|  | 			verb: "LOCK", | ||||||
|  | 			ops:  []int{int(OpLitPush), 0}, | ||||||
|  | 			pool: []string{"v1"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name.nested", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						wildcard{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			ops: []int{ | ||||||
|  | 				int(OpPush), operandFiller, | ||||||
|  | 				int(OpConcatN), 1, | ||||||
|  | 				int(OpCapture), 0, | ||||||
|  | 			}, | ||||||
|  | 			pool:   []string{"name.nested"}, | ||||||
|  | 			fields: []string{"name.nested"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				literal("obj"), | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name.nested", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						literal("a"), | ||||||
|  | 						wildcard{}, | ||||||
|  | 						literal("b"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				variable{ | ||||||
|  | 					path: "obj", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						deepWildcard{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			ops: []int{ | ||||||
|  | 				int(OpLitPush), 0, | ||||||
|  | 				int(OpLitPush), 1, | ||||||
|  | 				int(OpPush), operandFiller, | ||||||
|  | 				int(OpLitPush), 2, | ||||||
|  | 				int(OpConcatN), 3, | ||||||
|  | 				int(OpCapture), 3, | ||||||
|  | 				int(OpPushM), operandFiller, | ||||||
|  | 				int(OpConcatN), 1, | ||||||
|  | 				int(OpCapture), 0, | ||||||
|  | 			}, | ||||||
|  | 			pool:   []string{"obj", "a", "b", "name.nested"}, | ||||||
|  | 			fields: []string{"name.nested", "obj"}, | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		tmpl := template{ | ||||||
|  | 			segments: spec.segs, | ||||||
|  | 			verb:     spec.verb, | ||||||
|  | 		} | ||||||
|  | 		compiled := tmpl.Compile() | ||||||
|  | 		if got, want := compiled.Version, opcodeVersion; got != want { | ||||||
|  | 			t.Errorf("tmpl.Compile().Version = %d; want %d; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) | ||||||
|  | 		} | ||||||
|  | 		if got, want := compiled.OpCodes, spec.ops; !reflect.DeepEqual(got, want) { | ||||||
|  | 			t.Errorf("tmpl.Compile().OpCodes = %v; want %v; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) | ||||||
|  | 		} | ||||||
|  | 		if got, want := compiled.Pool, spec.pool; !reflect.DeepEqual(got, want) { | ||||||
|  | 			t.Errorf("tmpl.Compile().Pool = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) | ||||||
|  | 		} | ||||||
|  | 		if got, want := compiled.Verb, spec.verb; got != want { | ||||||
|  | 			t.Errorf("tmpl.Compile().Verb = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) | ||||||
|  | 		} | ||||||
|  | 		if got, want := compiled.Fields, spec.fields; !reflect.DeepEqual(got, want) { | ||||||
|  | 			t.Errorf("tmpl.Compile().Fields = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										363
									
								
								api/router/util/parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								api/router/util/parse.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,363 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/parse.go | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // InvalidTemplateError indicates that the path template is not valid. | ||||||
|  | type InvalidTemplateError struct { | ||||||
|  | 	tmpl string | ||||||
|  | 	msg  string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e InvalidTemplateError) Error() string { | ||||||
|  | 	return fmt.Sprintf("%s: %s", e.msg, e.tmpl) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Parse parses the string representation of path template | ||||||
|  | func Parse(tmpl string) (Compiler, error) { | ||||||
|  | 	if !strings.HasPrefix(tmpl, "/") { | ||||||
|  | 		return template{}, InvalidTemplateError{tmpl: tmpl, msg: "no leading /"} | ||||||
|  | 	} | ||||||
|  | 	tokens, verb := tokenize(tmpl[1:]) | ||||||
|  |  | ||||||
|  | 	p := parser{tokens: tokens} | ||||||
|  | 	segs, err := p.topLevelSegments() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return template{}, InvalidTemplateError{tmpl: tmpl, msg: err.Error()} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return template{ | ||||||
|  | 		segments: segs, | ||||||
|  | 		verb:     verb, | ||||||
|  | 		template: tmpl, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func tokenize(path string) (tokens []string, verb string) { | ||||||
|  | 	if path == "" { | ||||||
|  | 		return []string{eof}, "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const ( | ||||||
|  | 		init = iota | ||||||
|  | 		field | ||||||
|  | 		nested | ||||||
|  | 	) | ||||||
|  | 	var ( | ||||||
|  | 		st = init | ||||||
|  | 	) | ||||||
|  | 	for path != "" { | ||||||
|  | 		var idx int | ||||||
|  | 		switch st { | ||||||
|  | 		case init: | ||||||
|  | 			idx = strings.IndexAny(path, "/{") | ||||||
|  | 		case field: | ||||||
|  | 			idx = strings.IndexAny(path, ".=}") | ||||||
|  | 		case nested: | ||||||
|  | 			idx = strings.IndexAny(path, "/}") | ||||||
|  | 		} | ||||||
|  | 		if idx < 0 { | ||||||
|  | 			tokens = append(tokens, path) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		switch r := path[idx]; r { | ||||||
|  | 		case '/', '.': | ||||||
|  | 		case '{': | ||||||
|  | 			st = field | ||||||
|  | 		case '=': | ||||||
|  | 			st = nested | ||||||
|  | 		case '}': | ||||||
|  | 			st = init | ||||||
|  | 		} | ||||||
|  | 		if idx == 0 { | ||||||
|  | 			tokens = append(tokens, path[idx:idx+1]) | ||||||
|  | 		} else { | ||||||
|  | 			tokens = append(tokens, path[:idx], path[idx:idx+1]) | ||||||
|  | 		} | ||||||
|  | 		path = path[idx+1:] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	l := len(tokens) | ||||||
|  | 	t := tokens[l-1] | ||||||
|  | 	if idx := strings.LastIndex(t, ":"); idx == 0 { | ||||||
|  | 		tokens, verb = tokens[:l-1], t[1:] | ||||||
|  | 	} else if idx > 0 { | ||||||
|  | 		tokens[l-1], verb = t[:idx], t[idx+1:] | ||||||
|  | 	} | ||||||
|  | 	tokens = append(tokens, eof) | ||||||
|  | 	return tokens, verb | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // parser is a parser of the template syntax defined in github.com/googleapis/googleapis/google/api/http.proto. | ||||||
|  | type parser struct { | ||||||
|  | 	tokens   []string | ||||||
|  | 	accepted []string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // topLevelSegments is the target of this parser. | ||||||
|  | func (p *parser) topLevelSegments() ([]segment, error) { | ||||||
|  | 	if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 		logger.Debugf("Parsing %q", p.tokens) | ||||||
|  | 	} | ||||||
|  | 	segs, err := p.segments() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 		logger.Debugf("accept segments: %q; %q", p.accepted, p.tokens) | ||||||
|  | 	} | ||||||
|  | 	if _, err := p.accept(typeEOF); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unexpected token %q after segments %q", p.tokens[0], strings.Join(p.accepted, "")) | ||||||
|  | 	} | ||||||
|  | 	if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 		logger.Debugf("accept eof: %q; %q", p.accepted, p.tokens) | ||||||
|  | 	} | ||||||
|  | 	return segs, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *parser) segments() ([]segment, error) { | ||||||
|  | 	s, err := p.segment() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 		logger.Debugf("accept segment: %q; %q", p.accepted, p.tokens) | ||||||
|  | 	} | ||||||
|  | 	segs := []segment{s} | ||||||
|  | 	for { | ||||||
|  | 		if _, err := p.accept("/"); err != nil { | ||||||
|  | 			return segs, nil | ||||||
|  | 		} | ||||||
|  | 		s, err := p.segment() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return segs, err | ||||||
|  | 		} | ||||||
|  | 		segs = append(segs, s) | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Debugf("accept segment: %q; %q", p.accepted, p.tokens) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *parser) segment() (segment, error) { | ||||||
|  | 	if _, err := p.accept("*"); err == nil { | ||||||
|  | 		return wildcard{}, nil | ||||||
|  | 	} | ||||||
|  | 	if _, err := p.accept("**"); err == nil { | ||||||
|  | 		return deepWildcard{}, nil | ||||||
|  | 	} | ||||||
|  | 	if l, err := p.literal(); err == nil { | ||||||
|  | 		return l, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	v, err := p.variable() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("segment neither wildcards, literal or variable: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return v, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *parser) literal() (segment, error) { | ||||||
|  | 	lit, err := p.accept(typeLiteral) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return literal(lit), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *parser) variable() (segment, error) { | ||||||
|  | 	if _, err := p.accept("{"); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	path, err := p.fieldPath() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var segs []segment | ||||||
|  | 	if _, err := p.accept("="); err == nil { | ||||||
|  | 		segs, err = p.segments() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("invalid segment in variable %q: %v", path, err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		segs = []segment{wildcard{}} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if _, err := p.accept("}"); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unterminated variable segment: %s", path) | ||||||
|  | 	} | ||||||
|  | 	return variable{ | ||||||
|  | 		path:     path, | ||||||
|  | 		segments: segs, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *parser) fieldPath() (string, error) { | ||||||
|  | 	c, err := p.accept(typeIdent) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	components := []string{c} | ||||||
|  | 	for { | ||||||
|  | 		if _, err = p.accept("."); err != nil { | ||||||
|  | 			return strings.Join(components, "."), nil | ||||||
|  | 		} | ||||||
|  | 		c, err := p.accept(typeIdent) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", fmt.Errorf("invalid field path component: %v", err) | ||||||
|  | 		} | ||||||
|  | 		components = append(components, c) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // A termType is a type of terminal symbols. | ||||||
|  | type termType string | ||||||
|  |  | ||||||
|  | // These constants define some of valid values of termType. | ||||||
|  | // They improve readability of parse functions. | ||||||
|  | // | ||||||
|  | // You can also use "/", "*", "**", "." or "=" as valid values. | ||||||
|  | const ( | ||||||
|  | 	typeIdent   = termType("ident") | ||||||
|  | 	typeLiteral = termType("literal") | ||||||
|  | 	typeEOF     = termType("$") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// eof is the terminal symbol which always appears at the end of token sequence. | ||||||
|  | 	eof = "\u0000" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // accept tries to accept a token in "p". | ||||||
|  | // This function consumes a token and returns it if it matches to the specified "term". | ||||||
|  | // If it doesn't match, the function does not consume any tokens and return an error. | ||||||
|  | func (p *parser) accept(term termType) (string, error) { | ||||||
|  | 	t := p.tokens[0] | ||||||
|  | 	switch term { | ||||||
|  | 	case "/", "*", "**", ".", "=", "{", "}": | ||||||
|  | 		if t != string(term) && t != "/" { | ||||||
|  | 			return "", fmt.Errorf("expected %q but got %q", term, t) | ||||||
|  | 		} | ||||||
|  | 	case typeEOF: | ||||||
|  | 		if t != eof { | ||||||
|  | 			return "", fmt.Errorf("expected EOF but got %q", t) | ||||||
|  | 		} | ||||||
|  | 	case typeIdent: | ||||||
|  | 		if err := expectIdent(t); err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 	case typeLiteral: | ||||||
|  | 		if err := expectPChars(t); err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return "", fmt.Errorf("unknown termType %q", term) | ||||||
|  | 	} | ||||||
|  | 	p.tokens = p.tokens[1:] | ||||||
|  | 	p.accepted = append(p.accepted, t) | ||||||
|  | 	return t, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // expectPChars determines if "t" consists of only pchars defined in RFC3986. | ||||||
|  | // | ||||||
|  | // https://www.ietf.org/rfc/rfc3986.txt, P.49 | ||||||
|  | //   pchar         = unreserved / pct-encoded / sub-delims / ":" / "@" | ||||||
|  | //   unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~" | ||||||
|  | //   sub-delims    = "!" / "$" / "&" / "'" / "(" / ")" | ||||||
|  | //                 / "*" / "+" / "," / ";" / "=" | ||||||
|  | //   pct-encoded   = "%" HEXDIG HEXDIG | ||||||
|  | func expectPChars(t string) error { | ||||||
|  | 	const ( | ||||||
|  | 		init = iota | ||||||
|  | 		pct1 | ||||||
|  | 		pct2 | ||||||
|  | 	) | ||||||
|  | 	st := init | ||||||
|  | 	for _, r := range t { | ||||||
|  | 		if st != init { | ||||||
|  | 			if !isHexDigit(r) { | ||||||
|  | 				return fmt.Errorf("invalid hexdigit: %c(%U)", r, r) | ||||||
|  | 			} | ||||||
|  | 			switch st { | ||||||
|  | 			case pct1: | ||||||
|  | 				st = pct2 | ||||||
|  | 			case pct2: | ||||||
|  | 				st = init | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// unreserved | ||||||
|  | 		switch { | ||||||
|  | 		case 'A' <= r && r <= 'Z': | ||||||
|  | 			continue | ||||||
|  | 		case 'a' <= r && r <= 'z': | ||||||
|  | 			continue | ||||||
|  | 		case '0' <= r && r <= '9': | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		switch r { | ||||||
|  | 		case '-', '.', '_', '~': | ||||||
|  | 			// unreserved | ||||||
|  | 		case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': | ||||||
|  | 			// sub-delims | ||||||
|  | 		case ':', '@': | ||||||
|  | 			// rest of pchar | ||||||
|  | 		case '%': | ||||||
|  | 			// pct-encoded | ||||||
|  | 			st = pct1 | ||||||
|  | 		default: | ||||||
|  | 			return fmt.Errorf("invalid character in path segment: %q(%U)", r, r) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if st != init { | ||||||
|  | 		return fmt.Errorf("invalid percent-encoding in %q", t) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // expectIdent determines if "ident" is a valid identifier in .proto schema ([[:alpha:]_][[:alphanum:]_]*). | ||||||
|  | func expectIdent(ident string) error { | ||||||
|  | 	if ident == "" { | ||||||
|  | 		return fmt.Errorf("empty identifier") | ||||||
|  | 	} | ||||||
|  | 	for pos, r := range ident { | ||||||
|  | 		switch { | ||||||
|  | 		case '0' <= r && r <= '9': | ||||||
|  | 			if pos == 0 { | ||||||
|  | 				return fmt.Errorf("identifier starting with digit: %s", ident) | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		case 'A' <= r && r <= 'Z': | ||||||
|  | 			continue | ||||||
|  | 		case 'a' <= r && r <= 'z': | ||||||
|  | 			continue | ||||||
|  | 		case r == '_': | ||||||
|  | 			continue | ||||||
|  | 		default: | ||||||
|  | 			return fmt.Errorf("invalid character %q(%U) in identifier: %s", r, r, ident) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isHexDigit(r rune) bool { | ||||||
|  | 	switch { | ||||||
|  | 	case '0' <= r && r <= '9': | ||||||
|  | 		return true | ||||||
|  | 	case 'A' <= r && r <= 'F': | ||||||
|  | 		return true | ||||||
|  | 	case 'a' <= r && r <= 'f': | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
							
								
								
									
										321
									
								
								api/router/util/parse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								api/router/util/parse_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/parse_test.go | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestTokenize(t *testing.T) { | ||||||
|  | 	for _, spec := range []struct { | ||||||
|  | 		src    string | ||||||
|  | 		tokens []string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			src:    "", | ||||||
|  | 			tokens: []string{eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src:    "v1", | ||||||
|  | 			tokens: []string{"v1", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src:    "v1/b", | ||||||
|  | 			tokens: []string{"v1", "/", "b", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src:    "v1/endpoint/*", | ||||||
|  | 			tokens: []string{"v1", "/", "endpoint", "/", "*", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src:    "v1/endpoint/**", | ||||||
|  | 			tokens: []string{"v1", "/", "endpoint", "/", "**", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src: "v1/b/{bucket_name=*}", | ||||||
|  | 			tokens: []string{ | ||||||
|  | 				"v1", "/", | ||||||
|  | 				"b", "/", | ||||||
|  | 				"{", "bucket_name", "=", "*", "}", | ||||||
|  | 				eof, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src: "v1/b/{bucket_name=buckets/*}", | ||||||
|  | 			tokens: []string{ | ||||||
|  | 				"v1", "/", | ||||||
|  | 				"b", "/", | ||||||
|  | 				"{", "bucket_name", "=", "buckets", "/", "*", "}", | ||||||
|  | 				eof, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src: "v1/b/{bucket_name=buckets/*}/o", | ||||||
|  | 			tokens: []string{ | ||||||
|  | 				"v1", "/", | ||||||
|  | 				"b", "/", | ||||||
|  | 				"{", "bucket_name", "=", "buckets", "/", "*", "}", "/", | ||||||
|  | 				"o", | ||||||
|  | 				eof, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src: "v1/b/{bucket_name=buckets/*}/o/{name}", | ||||||
|  | 			tokens: []string{ | ||||||
|  | 				"v1", "/", | ||||||
|  | 				"b", "/", | ||||||
|  | 				"{", "bucket_name", "=", "buckets", "/", "*", "}", "/", | ||||||
|  | 				"o", "/", "{", "name", "}", | ||||||
|  | 				eof, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			src: "v1/a=b&c=d;e=f:g/endpoint.rdf", | ||||||
|  | 			tokens: []string{ | ||||||
|  | 				"v1", "/", | ||||||
|  | 				"a=b&c=d;e=f:g", "/", | ||||||
|  | 				"endpoint.rdf", | ||||||
|  | 				eof, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		tokens, verb := tokenize(spec.src) | ||||||
|  | 		if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) { | ||||||
|  | 			t.Errorf("tokenize(%q) = %q, _; want %q, _", spec.src, got, want) | ||||||
|  | 		} | ||||||
|  | 		if got, want := verb, ""; got != want { | ||||||
|  | 			t.Errorf("tokenize(%q) = _, %q; want _, %q", spec.src, got, want) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		src := fmt.Sprintf("%s:%s", spec.src, "LOCK") | ||||||
|  | 		tokens, verb = tokenize(src) | ||||||
|  | 		if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) { | ||||||
|  | 			t.Errorf("tokenize(%q) = %q, _; want %q, _", src, got, want) | ||||||
|  | 		} | ||||||
|  | 		if got, want := verb, "LOCK"; got != want { | ||||||
|  | 			t.Errorf("tokenize(%q) = _, %q; want _, %q", src, got, want) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestParseSegments(t *testing.T) { | ||||||
|  | 	flag.Set("v", "3") | ||||||
|  | 	for _, spec := range []struct { | ||||||
|  | 		tokens []string | ||||||
|  | 		want   []segment | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"v1", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"/", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				wildcard{}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"-._~!$&'()*+,;=:@", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				literal("-._~!$&'()*+,;=:@"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"%e7%ac%ac%e4%b8%80%e7%89%88", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				literal("%e7%ac%ac%e4%b8%80%e7%89%88"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"v1", "/", "*", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 				wildcard{}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"v1", "/", "**", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 				deepWildcard{}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"{", "name", "}", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						wildcard{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"{", "name", "=", "*", "}", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						wildcard{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"{", "field", ".", "nested", ".", "nested2", "=", "*", "}", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				variable{ | ||||||
|  | 					path: "field.nested.nested2", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						wildcard{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{"{", "name", "=", "a", "/", "b", "/", "*", "}", eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						literal("a"), | ||||||
|  | 						literal("b"), | ||||||
|  | 						wildcard{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			tokens: []string{ | ||||||
|  | 				"v1", "/", | ||||||
|  | 				"{", | ||||||
|  | 				"name", ".", "nested", ".", "nested2", | ||||||
|  | 				"=", | ||||||
|  | 				"a", "/", "b", "/", "*", | ||||||
|  | 				"}", "/", | ||||||
|  | 				"o", "/", | ||||||
|  | 				"{", | ||||||
|  | 				"another_name", | ||||||
|  | 				"=", | ||||||
|  | 				"a", "/", "b", "/", "*", "/", "c", | ||||||
|  | 				"}", "/", | ||||||
|  | 				"**", | ||||||
|  | 				eof}, | ||||||
|  | 			want: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name.nested.nested2", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						literal("a"), | ||||||
|  | 						literal("b"), | ||||||
|  | 						wildcard{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				literal("o"), | ||||||
|  | 				variable{ | ||||||
|  | 					path: "another_name", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						literal("a"), | ||||||
|  | 						literal("b"), | ||||||
|  | 						wildcard{}, | ||||||
|  | 						literal("c"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				deepWildcard{}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		p := parser{tokens: spec.tokens} | ||||||
|  | 		segs, err := p.topLevelSegments() | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Errorf("parser{%q}.segments() failed with %v; want success", spec.tokens, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if got, want := segs, spec.want; !reflect.DeepEqual(got, want) { | ||||||
|  | 			t.Errorf("parser{%q}.segments() = %#v; want %#v", spec.tokens, got, want) | ||||||
|  | 		} | ||||||
|  | 		if got := p.tokens; len(got) > 0 { | ||||||
|  | 			t.Errorf("p.tokens = %q; want []; spec.tokens=%q", got, spec.tokens) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestParseSegmentsWithErrors(t *testing.T) { | ||||||
|  | 	flag.Set("v", "3") | ||||||
|  | 	for _, spec := range []struct { | ||||||
|  | 		tokens []string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			// double slash | ||||||
|  | 			tokens: []string{"//", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// invalid literal | ||||||
|  | 			tokens: []string{"a?b", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// invalid percent-encoding | ||||||
|  | 			tokens: []string{"%", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// invalid percent-encoding | ||||||
|  | 			tokens: []string{"%2", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// invalid percent-encoding | ||||||
|  | 			tokens: []string{"a%2z", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// empty segments | ||||||
|  | 			tokens: []string{eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// unterminated variable | ||||||
|  | 			tokens: []string{"{", "name", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// unterminated variable | ||||||
|  | 			tokens: []string{"{", "name", "=", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// unterminated variable | ||||||
|  | 			tokens: []string{"{", "name", "=", "*", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// empty component in field path | ||||||
|  | 			tokens: []string{"{", "name", ".", "}", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// empty component in field path | ||||||
|  | 			tokens: []string{"{", "name", ".", ".", "nested", "}", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// invalid character in identifier | ||||||
|  | 			tokens: []string{"{", "field-name", "}", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// no slash between segments | ||||||
|  | 			tokens: []string{"v1", "endpoint", eof}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// no slash between segments | ||||||
|  | 			tokens: []string{"v1", "{", "name", "}", eof}, | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		p := parser{tokens: spec.tokens} | ||||||
|  | 		segs, err := p.topLevelSegments() | ||||||
|  | 		if err == nil { | ||||||
|  | 			t.Errorf("parser{%q}.segments() succeeded; want InvalidTemplateError; accepted %#v", spec.tokens, segs) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		logger.Info(err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								api/router/util/pattern.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								api/router/util/pattern.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/utilities/pattern.go | ||||||
|  |  | ||||||
|  | // An OpCode is a opcode of compiled path patterns. | ||||||
|  | type OpCode int | ||||||
|  |  | ||||||
|  | // These constants are the valid values of OpCode. | ||||||
|  | const ( | ||||||
|  | 	// OpNop does nothing | ||||||
|  | 	OpNop = OpCode(iota) | ||||||
|  | 	// OpPush pushes a component to stack | ||||||
|  | 	OpPush | ||||||
|  | 	// OpLitPush pushes a component to stack if it matches to the literal | ||||||
|  | 	OpLitPush | ||||||
|  | 	// OpPushM concatenates the remaining components and pushes it to stack | ||||||
|  | 	OpPushM | ||||||
|  | 	// OpConcatN pops N items from stack, concatenates them and pushes it back to stack | ||||||
|  | 	OpConcatN | ||||||
|  | 	// OpCapture pops an item and binds it to the variable | ||||||
|  | 	OpCapture | ||||||
|  | 	// OpEnd is the least positive invalid opcode. | ||||||
|  | 	OpEnd | ||||||
|  | ) | ||||||
							
								
								
									
										283
									
								
								api/router/util/runtime.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								api/router/util/runtime.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/runtime/pattern.go | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// ErrNotMatch indicates that the given HTTP request path does not match to the pattern. | ||||||
|  | 	ErrNotMatch = errors.New("not match to the path pattern") | ||||||
|  | 	// ErrInvalidPattern indicates that the given definition of Pattern is not valid. | ||||||
|  | 	ErrInvalidPattern = errors.New("invalid pattern") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type rop struct { | ||||||
|  | 	code    OpCode | ||||||
|  | 	operand int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Pattern is a template pattern of http request paths defined in github.com/googleapis/googleapis/google/api/http.proto. | ||||||
|  | type Pattern struct { | ||||||
|  | 	// ops is a list of operations | ||||||
|  | 	ops []rop | ||||||
|  | 	// pool is a constant pool indexed by the operands or vars. | ||||||
|  | 	pool []string | ||||||
|  | 	// vars is a list of variables names to be bound by this pattern | ||||||
|  | 	vars []string | ||||||
|  | 	// stacksize is the max depth of the stack | ||||||
|  | 	stacksize int | ||||||
|  | 	// tailLen is the length of the fixed-size segments after a deep wildcard | ||||||
|  | 	tailLen int | ||||||
|  | 	// verb is the VERB part of the path pattern. It is empty if the pattern does not have VERB part. | ||||||
|  | 	verb string | ||||||
|  | 	// assumeColonVerb indicates whether a path suffix after a final | ||||||
|  | 	// colon may only be interpreted as a verb. | ||||||
|  | 	assumeColonVerb bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type patternOptions struct { | ||||||
|  | 	assumeColonVerb bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // PatternOpt is an option for creating Patterns. | ||||||
|  | type PatternOpt func(*patternOptions) | ||||||
|  |  | ||||||
|  | // NewPattern returns a new Pattern from the given definition values. | ||||||
|  | // "ops" is a sequence of op codes. "pool" is a constant pool. | ||||||
|  | // "verb" is the verb part of the pattern. It is empty if the pattern does not have the part. | ||||||
|  | // "version" must be 1 for now. | ||||||
|  | // It returns an error if the given definition is invalid. | ||||||
|  | func NewPattern(version int, ops []int, pool []string, verb string, opts ...PatternOpt) (Pattern, error) { | ||||||
|  | 	options := patternOptions{ | ||||||
|  | 		assumeColonVerb: true, | ||||||
|  | 	} | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if version != 1 { | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Debugf("unsupported version: %d", version) | ||||||
|  | 		} | ||||||
|  | 		return Pattern{}, ErrInvalidPattern | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	l := len(ops) | ||||||
|  | 	if l%2 != 0 { | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Debugf("odd number of ops codes: %d", l) | ||||||
|  | 		} | ||||||
|  | 		return Pattern{}, ErrInvalidPattern | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		typedOps        []rop | ||||||
|  | 		stack, maxstack int | ||||||
|  | 		tailLen         int | ||||||
|  | 		pushMSeen       bool | ||||||
|  | 		vars            []string | ||||||
|  | 	) | ||||||
|  | 	for i := 0; i < l; i += 2 { | ||||||
|  | 		op := rop{code: OpCode(ops[i]), operand: ops[i+1]} | ||||||
|  | 		switch op.code { | ||||||
|  | 		case OpNop: | ||||||
|  | 			continue | ||||||
|  | 		case OpPush: | ||||||
|  | 			if pushMSeen { | ||||||
|  | 				tailLen++ | ||||||
|  | 			} | ||||||
|  | 			stack++ | ||||||
|  | 		case OpPushM: | ||||||
|  | 			if pushMSeen { | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debug("pushM appears twice") | ||||||
|  | 				} | ||||||
|  | 				return Pattern{}, ErrInvalidPattern | ||||||
|  | 			} | ||||||
|  | 			pushMSeen = true | ||||||
|  | 			stack++ | ||||||
|  | 		case OpLitPush: | ||||||
|  | 			if op.operand < 0 || len(pool) <= op.operand { | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debugf("negative literal index: %d", op.operand) | ||||||
|  | 				} | ||||||
|  | 				return Pattern{}, ErrInvalidPattern | ||||||
|  | 			} | ||||||
|  | 			if pushMSeen { | ||||||
|  | 				tailLen++ | ||||||
|  | 			} | ||||||
|  | 			stack++ | ||||||
|  | 		case OpConcatN: | ||||||
|  | 			if op.operand <= 0 { | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debugf("negative concat size: %d", op.operand) | ||||||
|  | 				} | ||||||
|  | 				return Pattern{}, ErrInvalidPattern | ||||||
|  | 			} | ||||||
|  | 			stack -= op.operand | ||||||
|  | 			if stack < 0 { | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debug("stack underflow") | ||||||
|  | 				} | ||||||
|  | 				return Pattern{}, ErrInvalidPattern | ||||||
|  | 			} | ||||||
|  | 			stack++ | ||||||
|  | 		case OpCapture: | ||||||
|  | 			if op.operand < 0 || len(pool) <= op.operand { | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debugf("variable name index out of bound: %d", op.operand) | ||||||
|  | 				} | ||||||
|  | 				return Pattern{}, ErrInvalidPattern | ||||||
|  | 			} | ||||||
|  | 			v := pool[op.operand] | ||||||
|  | 			op.operand = len(vars) | ||||||
|  | 			vars = append(vars, v) | ||||||
|  | 			stack-- | ||||||
|  | 			if stack < 0 { | ||||||
|  | 				if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 					logger.Debug("stack underflow") | ||||||
|  | 				} | ||||||
|  | 				return Pattern{}, ErrInvalidPattern | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 			if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 				logger.Debugf("invalid opcode: %d", op.code) | ||||||
|  | 			} | ||||||
|  | 			return Pattern{}, ErrInvalidPattern | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if maxstack < stack { | ||||||
|  | 			maxstack = stack | ||||||
|  | 		} | ||||||
|  | 		typedOps = append(typedOps, op) | ||||||
|  | 	} | ||||||
|  | 	return Pattern{ | ||||||
|  | 		ops:             typedOps, | ||||||
|  | 		pool:            pool, | ||||||
|  | 		vars:            vars, | ||||||
|  | 		stacksize:       maxstack, | ||||||
|  | 		tailLen:         tailLen, | ||||||
|  | 		verb:            verb, | ||||||
|  | 		assumeColonVerb: options.assumeColonVerb, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MustPattern is a helper function which makes it easier to call NewPattern in variable initialization. | ||||||
|  | func MustPattern(p Pattern, err error) Pattern { | ||||||
|  | 	if err != nil { | ||||||
|  | 		if logger.V(logger.DebugLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Fatalf("Pattern initialization failed: %v", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Match examines components if it matches to the Pattern. | ||||||
|  | // If it matches, the function returns a mapping from field paths to their captured values. | ||||||
|  | // If otherwise, the function returns an error. | ||||||
|  | func (p Pattern) Match(components []string, verb string) (map[string]string, error) { | ||||||
|  | 	if p.verb != verb { | ||||||
|  | 		if p.assumeColonVerb || p.verb != "" { | ||||||
|  | 			return nil, ErrNotMatch | ||||||
|  | 		} | ||||||
|  | 		if len(components) == 0 { | ||||||
|  | 			components = []string{":" + verb} | ||||||
|  | 		} else { | ||||||
|  | 			components = append([]string{}, components...) | ||||||
|  | 			components[len(components)-1] += ":" + verb | ||||||
|  | 		} | ||||||
|  | 		verb = "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var pos int | ||||||
|  | 	stack := make([]string, 0, p.stacksize) | ||||||
|  | 	captured := make([]string, len(p.vars)) | ||||||
|  | 	l := len(components) | ||||||
|  | 	for _, op := range p.ops { | ||||||
|  | 		switch op.code { | ||||||
|  | 		case OpNop: | ||||||
|  | 			continue | ||||||
|  | 		case OpPush, OpLitPush: | ||||||
|  | 			if pos >= l { | ||||||
|  | 				return nil, ErrNotMatch | ||||||
|  | 			} | ||||||
|  | 			c := components[pos] | ||||||
|  | 			if op.code == OpLitPush { | ||||||
|  | 				if lit := p.pool[op.operand]; c != lit { | ||||||
|  | 					return nil, ErrNotMatch | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			stack = append(stack, c) | ||||||
|  | 			pos++ | ||||||
|  | 		case OpPushM: | ||||||
|  | 			end := len(components) | ||||||
|  | 			if end < pos+p.tailLen { | ||||||
|  | 				return nil, ErrNotMatch | ||||||
|  | 			} | ||||||
|  | 			end -= p.tailLen | ||||||
|  | 			stack = append(stack, strings.Join(components[pos:end], "/")) | ||||||
|  | 			pos = end | ||||||
|  | 		case OpConcatN: | ||||||
|  | 			n := op.operand | ||||||
|  | 			l := len(stack) - n | ||||||
|  | 			stack = append(stack[:l], strings.Join(stack[l:], "/")) | ||||||
|  | 		case OpCapture: | ||||||
|  | 			n := len(stack) - 1 | ||||||
|  | 			captured[op.operand] = stack[n] | ||||||
|  | 			stack = stack[:n] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if pos < l { | ||||||
|  | 		return nil, ErrNotMatch | ||||||
|  | 	} | ||||||
|  | 	bindings := make(map[string]string) | ||||||
|  | 	for i, val := range captured { | ||||||
|  | 		bindings[p.vars[i]] = val | ||||||
|  | 	} | ||||||
|  | 	return bindings, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Verb returns the verb part of the Pattern. | ||||||
|  | func (p Pattern) Verb() string { return p.verb } | ||||||
|  |  | ||||||
|  | func (p Pattern) String() string { | ||||||
|  | 	var stack []string | ||||||
|  | 	for _, op := range p.ops { | ||||||
|  | 		switch op.code { | ||||||
|  | 		case OpNop: | ||||||
|  | 			continue | ||||||
|  | 		case OpPush: | ||||||
|  | 			stack = append(stack, "*") | ||||||
|  | 		case OpLitPush: | ||||||
|  | 			stack = append(stack, p.pool[op.operand]) | ||||||
|  | 		case OpPushM: | ||||||
|  | 			stack = append(stack, "**") | ||||||
|  | 		case OpConcatN: | ||||||
|  | 			n := op.operand | ||||||
|  | 			l := len(stack) - n | ||||||
|  | 			stack = append(stack[:l], strings.Join(stack[l:], "/")) | ||||||
|  | 		case OpCapture: | ||||||
|  | 			n := len(stack) - 1 | ||||||
|  | 			stack[n] = fmt.Sprintf("{%s=%s}", p.vars[op.operand], stack[n]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	segs := strings.Join(stack, "/") | ||||||
|  | 	if p.verb != "" { | ||||||
|  | 		return fmt.Sprintf("/%s:%s", segs, p.verb) | ||||||
|  | 	} | ||||||
|  | 	return "/" + segs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AssumeColonVerbOpt indicates whether a path suffix after a final | ||||||
|  | // colon may only be interpreted as a verb. | ||||||
|  | func AssumeColonVerbOpt(val bool) PatternOpt { | ||||||
|  | 	return PatternOpt(func(o *patternOptions) { | ||||||
|  | 		o.assumeColonVerb = val | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								api/router/util/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								api/router/util/types.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/types.go | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type template struct { | ||||||
|  | 	segments []segment | ||||||
|  | 	verb     string | ||||||
|  | 	template string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type segment interface { | ||||||
|  | 	fmt.Stringer | ||||||
|  | 	compile() (ops []op) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type wildcard struct{} | ||||||
|  |  | ||||||
|  | type deepWildcard struct{} | ||||||
|  |  | ||||||
|  | type literal string | ||||||
|  |  | ||||||
|  | type variable struct { | ||||||
|  | 	path     string | ||||||
|  | 	segments []segment | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (wildcard) String() string { | ||||||
|  | 	return "*" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (deepWildcard) String() string { | ||||||
|  | 	return "**" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l literal) String() string { | ||||||
|  | 	return string(l) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (v variable) String() string { | ||||||
|  | 	var segs []string | ||||||
|  | 	for _, s := range v.segments { | ||||||
|  | 		segs = append(segs, s.String()) | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("{%s=%s}", v.path, strings.Join(segs, "/")) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t template) String() string { | ||||||
|  | 	var segs []string | ||||||
|  | 	for _, s := range t.segments { | ||||||
|  | 		segs = append(segs, s.String()) | ||||||
|  | 	} | ||||||
|  | 	str := strings.Join(segs, "/") | ||||||
|  | 	if t.verb != "" { | ||||||
|  | 		str = fmt.Sprintf("%s:%s", str, t.verb) | ||||||
|  | 	} | ||||||
|  | 	return "/" + str | ||||||
|  | } | ||||||
							
								
								
									
										93
									
								
								api/router/util/types_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								api/router/util/types_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | package util | ||||||
|  |  | ||||||
|  | // download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/types_test.go | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestTemplateStringer(t *testing.T) { | ||||||
|  | 	for _, spec := range []struct { | ||||||
|  | 		segs []segment | ||||||
|  | 		want string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 			}, | ||||||
|  | 			want: "/v1", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				wildcard{}, | ||||||
|  | 			}, | ||||||
|  | 			want: "/*", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				deepWildcard{}, | ||||||
|  | 			}, | ||||||
|  | 			want: "/**", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						literal("a"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: "/{name=a}", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						literal("a"), | ||||||
|  | 						wildcard{}, | ||||||
|  | 						literal("b"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: "/{name=a/*/b}", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			segs: []segment{ | ||||||
|  | 				literal("v1"), | ||||||
|  | 				variable{ | ||||||
|  | 					path: "name", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						literal("a"), | ||||||
|  | 						wildcard{}, | ||||||
|  | 						literal("b"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				literal("c"), | ||||||
|  | 				variable{ | ||||||
|  | 					path: "field.nested", | ||||||
|  | 					segments: []segment{ | ||||||
|  | 						wildcard{}, | ||||||
|  | 						literal("d"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				wildcard{}, | ||||||
|  | 				literal("e"), | ||||||
|  | 				deepWildcard{}, | ||||||
|  | 			}, | ||||||
|  | 			want: "/v1/{name=a/*/b}/c/{field.nested=*/d}/*/e/**", | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		tmpl := template{segments: spec.segs} | ||||||
|  | 		if got, want := tmpl.String(), spec.want; got != want { | ||||||
|  | 			t.Errorf("%#v.String() = %q; want %q", tmpl, got, want) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		tmpl.verb = "LOCK" | ||||||
|  | 		if got, want := tmpl.String(), fmt.Sprintf("%s:LOCK", spec.want); got != want { | ||||||
|  | 			t.Errorf("%#v.String() = %q; want %q", tmpl, got, want) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ | |||||||
| package acme | package acme | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"crypto/tls" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"net" | 	"net" | ||||||
| ) | ) | ||||||
| @@ -14,7 +15,10 @@ var ( | |||||||
|  |  | ||||||
| // Provider is a ACME provider interface | // Provider is a ACME provider interface | ||||||
| type Provider interface { | type Provider interface { | ||||||
| 	NewListener(...string) (net.Listener, error) | 	// Listen returns a new listener | ||||||
|  | 	Listen(...string) (net.Listener, error) | ||||||
|  | 	// TLSConfig returns a tls config | ||||||
|  | 	TLSConfig(...string) (*tls.Config, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| // The Let's Encrypt ACME endpoints | // The Let's Encrypt ACME endpoints | ||||||
|   | |||||||
| @@ -3,21 +3,44 @@ | |||||||
| package autocert | package autocert | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"crypto/tls" | ||||||
| 	"net" | 	"net" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api/server/acme" | 	"github.com/micro/go-micro/v2/api/server/acme" | ||||||
|  | 	"github.com/micro/go-micro/v2/logger" | ||||||
| 	"golang.org/x/crypto/acme/autocert" | 	"golang.org/x/crypto/acme/autocert" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // autoCertACME is the ACME provider from golang.org/x/crypto/acme/autocert | // autoCertACME is the ACME provider from golang.org/x/crypto/acme/autocert | ||||||
| type autocertProvider struct{} | type autocertProvider struct{} | ||||||
|  |  | ||||||
| // NewListener implements acme.Provider | // Listen implements acme.Provider | ||||||
| func (a *autocertProvider) NewListener(ACMEHosts ...string) (net.Listener, error) { | func (a *autocertProvider) Listen(hosts ...string) (net.Listener, error) { | ||||||
| 	return autocert.NewListener(ACMEHosts...), nil | 	return autocert.NewListener(hosts...), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TLSConfig returns a new tls config | ||||||
|  | func (a *autocertProvider) TLSConfig(hosts ...string) (*tls.Config, error) { | ||||||
|  | 	// create a new manager | ||||||
|  | 	m := &autocert.Manager{ | ||||||
|  | 		Prompt: autocert.AcceptTOS, | ||||||
|  | 	} | ||||||
|  | 	if len(hosts) > 0 { | ||||||
|  | 		m.HostPolicy = autocert.HostWhitelist(hosts...) | ||||||
|  | 	} | ||||||
|  | 	dir := cacheDir() | ||||||
|  | 	if err := os.MkdirAll(dir, 0700); err != nil { | ||||||
|  | 		if logger.V(logger.InfoLevel, logger.DefaultLogger) { | ||||||
|  | 			logger.Infof("warning: autocert not using a cache: %v", err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		m.Cache = autocert.DirCache(dir) | ||||||
|  | 	} | ||||||
|  | 	return m.TLSConfig(), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // New returns an autocert acme.Provider | // New returns an autocert acme.Provider | ||||||
| func New() acme.Provider { | func NewProvider() acme.Provider { | ||||||
| 	return &autocertProvider{} | 	return &autocertProvider{} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,9 +5,9 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestAutocert(t *testing.T) { | func TestAutocert(t *testing.T) { | ||||||
| 	l := New() | 	l := NewProvider() | ||||||
| 	if _, ok := l.(*autocertProvider); !ok { | 	if _, ok := l.(*autocertProvider); !ok { | ||||||
| 		t.Error("New() didn't return an autocertProvider") | 		t.Error("NewProvider() didn't return an autocertProvider") | ||||||
| 	} | 	} | ||||||
| 	// TODO: Travis CI doesn't let us bind :443 | 	// TODO: Travis CI doesn't let us bind :443 | ||||||
| 	// if _, err := l.NewListener(); err != nil { | 	// if _, err := l.NewListener(); err != nil { | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								api/server/acme/autocert/cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								api/server/acme/autocert/cache.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | package autocert | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"runtime" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func homeDir() string { | ||||||
|  | 	if runtime.GOOS == "windows" { | ||||||
|  | 		return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") | ||||||
|  | 	} | ||||||
|  | 	if h := os.Getenv("HOME"); h != "" { | ||||||
|  | 		return h | ||||||
|  | 	} | ||||||
|  | 	return "/" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func cacheDir() string { | ||||||
|  | 	const base = "golang-autocert" | ||||||
|  | 	switch runtime.GOOS { | ||||||
|  | 	case "darwin": | ||||||
|  | 		return filepath.Join(homeDir(), "Library", "Caches", base) | ||||||
|  | 	case "windows": | ||||||
|  | 		for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} { | ||||||
|  | 			if v := os.Getenv(ev); v != "" { | ||||||
|  | 				return filepath.Join(v, base) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		// Worst case: | ||||||
|  | 		return filepath.Join(homeDir(), base) | ||||||
|  | 	} | ||||||
|  | 	if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { | ||||||
|  | 		return filepath.Join(xdg, base) | ||||||
|  | 	} | ||||||
|  | 	return filepath.Join(homeDir(), ".cache", base) | ||||||
|  | } | ||||||
| @@ -1,26 +1,27 @@ | |||||||
| // Package certmagic is the ACME provider from github.com/mholt/certmagic | // Package certmagic is the ACME provider from github.com/caddyserver/certmagic | ||||||
| package certmagic | package certmagic | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"log" | 	"crypto/tls" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"net" | 	"net" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/mholt/certmagic" | 	"github.com/caddyserver/certmagic" | ||||||
|  | 	"github.com/micro/go-micro/v2/api/server/acme" | ||||||
| 	"github.com/micro/go-micro/api/server/acme" | 	"github.com/micro/go-micro/v2/logger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type certmagicProvider struct { | type certmagicProvider struct { | ||||||
| 	opts acme.Options | 	opts acme.Options | ||||||
| } | } | ||||||
|  |  | ||||||
| func (c *certmagicProvider) NewListener(ACMEHosts ...string) (net.Listener, error) { | // TODO: set self-contained options | ||||||
| 	certmagic.Default.CA = c.opts.CA | func (c *certmagicProvider) setup() { | ||||||
|  | 	certmagic.DefaultACME.CA = c.opts.CA | ||||||
| 	if c.opts.ChallengeProvider != nil { | 	if c.opts.ChallengeProvider != nil { | ||||||
| 		// Enabling DNS Challenge disables the other challenges | 		// Enabling DNS Challenge disables the other challenges | ||||||
| 		certmagic.Default.DNSProvider = c.opts.ChallengeProvider | 		certmagic.DefaultACME.DNSProvider = c.opts.ChallengeProvider | ||||||
| 	} | 	} | ||||||
| 	if c.opts.OnDemand { | 	if c.opts.OnDemand { | ||||||
| 		certmagic.Default.OnDemand = new(certmagic.OnDemandConfig) | 		certmagic.Default.OnDemand = new(certmagic.OnDemandConfig) | ||||||
| @@ -31,15 +32,24 @@ func (c *certmagicProvider) NewListener(ACMEHosts ...string) (net.Listener, erro | |||||||
| 	} | 	} | ||||||
| 	// If multiple instances of the provider are running, inject some | 	// If multiple instances of the provider are running, inject some | ||||||
| 	// randomness so they don't collide | 	// randomness so they don't collide | ||||||
|  | 	// RenewalWindowRatio [0.33 - 0.50) | ||||||
| 	rand.Seed(time.Now().UnixNano()) | 	rand.Seed(time.Now().UnixNano()) | ||||||
| 	randomDuration := (7 * 24 * time.Hour) + (time.Duration(rand.Intn(504)) * time.Hour) | 	randomRatio := float64(rand.Intn(17) + 33) * 0.01 | ||||||
| 	certmagic.Default.RenewDurationBefore = randomDuration | 	certmagic.Default.RenewalWindowRatio = randomRatio | ||||||
|  |  | ||||||
| 	return certmagic.Listen(ACMEHosts) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // New returns a certmagic provider | func (c *certmagicProvider) Listen(hosts ...string) (net.Listener, error) { | ||||||
| func New(options ...acme.Option) acme.Provider { | 	c.setup() | ||||||
|  | 	return certmagic.Listen(hosts) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *certmagicProvider) TLSConfig(hosts ...string) (*tls.Config, error) { | ||||||
|  | 	c.setup() | ||||||
|  | 	return certmagic.TLS(hosts) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewProvider returns a certmagic provider | ||||||
|  | func NewProvider(options ...acme.Option) acme.Provider { | ||||||
| 	opts := acme.DefaultOptions() | 	opts := acme.DefaultOptions() | ||||||
|  |  | ||||||
| 	for _, o := range options { | 	for _, o := range options { | ||||||
| @@ -48,7 +58,7 @@ func New(options ...acme.Option) acme.Provider { | |||||||
|  |  | ||||||
| 	if opts.Cache != nil { | 	if opts.Cache != nil { | ||||||
| 		if _, ok := opts.Cache.(certmagic.Storage); !ok { | 		if _, ok := opts.Cache.(certmagic.Storage); !ok { | ||||||
| 			log.Fatal("ACME: cache provided doesn't implement certmagic's Storage interface") | 			logger.Fatal("ACME: cache provided doesn't implement certmagic's Storage interface") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,224 +0,0 @@ | |||||||
| package certmagic |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"net/http" |  | ||||||
| 	"os" |  | ||||||
| 	"reflect" |  | ||||||
| 	"sort" |  | ||||||
| 	"testing" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/go-acme/lego/v3/providers/dns/cloudflare" |  | ||||||
| 	"github.com/mholt/certmagic" |  | ||||||
| 	"github.com/micro/go-micro/api/server/acme" |  | ||||||
| 	cfstore "github.com/micro/go-micro/store/cloudflare" |  | ||||||
| 	"github.com/micro/go-micro/sync/lock/memory" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestCertMagic(t *testing.T) { |  | ||||||
| 	if len(os.Getenv("IN_TRAVIS_CI")) != 0 { |  | ||||||
| 		t.Skip("Travis doesn't let us bind :443") |  | ||||||
| 	} |  | ||||||
| 	l, err := New().NewListener() |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err.Error()) |  | ||||||
| 	} |  | ||||||
| 	l.Close() |  | ||||||
|  |  | ||||||
| 	c := cloudflare.NewDefaultConfig() |  | ||||||
| 	c.AuthEmail = "" |  | ||||||
| 	c.AuthKey = "" |  | ||||||
| 	c.AuthToken = "test" |  | ||||||
| 	c.ZoneToken = "test" |  | ||||||
|  |  | ||||||
| 	p, err := cloudflare.NewDNSProviderConfig(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err.Error()) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	l, err = New(acme.AcceptToS(true), |  | ||||||
| 		acme.CA(acme.LetsEncryptStagingCA), |  | ||||||
| 		acme.ChallengeProvider(p), |  | ||||||
| 	).NewListener() |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err.Error()) |  | ||||||
| 	} |  | ||||||
| 	l.Close() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStorageImplementation(t *testing.T) { |  | ||||||
| 	apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID") |  | ||||||
| 	kvID := os.Getenv("KV_NAMESPACE_ID") |  | ||||||
| 	if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 { |  | ||||||
| 		t.Skip("No Cloudflare API keys available, skipping test") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var s certmagic.Storage |  | ||||||
| 	st := cfstore.NewStore( |  | ||||||
| 		cfstore.Token(apiToken), |  | ||||||
| 		cfstore.Account(accountID), |  | ||||||
| 		cfstore.Namespace(kvID), |  | ||||||
| 	) |  | ||||||
| 	s = &storage{ |  | ||||||
| 		lock:  memory.NewLock(), |  | ||||||
| 		store: st, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test Lock |  | ||||||
| 	if err := s.Lock("test"); err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test Unlock |  | ||||||
| 	if err := s.Unlock("test"); err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test data |  | ||||||
| 	testdata := []struct { |  | ||||||
| 		key   string |  | ||||||
| 		value []byte |  | ||||||
| 	}{ |  | ||||||
| 		{key: "/foo/a", value: []byte("lorem")}, |  | ||||||
| 		{key: "/foo/b", value: []byte("ipsum")}, |  | ||||||
| 		{key: "/foo/c", value: []byte("dolor")}, |  | ||||||
| 		{key: "/foo/d", value: []byte("sit")}, |  | ||||||
| 		{key: "/bar/a", value: []byte("amet")}, |  | ||||||
| 		{key: "/bar/b", value: []byte("consectetur")}, |  | ||||||
| 		{key: "/bar/c", value: []byte("adipiscing")}, |  | ||||||
| 		{key: "/bar/d", value: []byte("elit")}, |  | ||||||
| 		{key: "/foo/bar/a", value: []byte("sed")}, |  | ||||||
| 		{key: "/foo/bar/b", value: []byte("do")}, |  | ||||||
| 		{key: "/foo/bar/c", value: []byte("eiusmod")}, |  | ||||||
| 		{key: "/foo/bar/d", value: []byte("tempor")}, |  | ||||||
| 		{key: "/foo/bar/baz/a", value: []byte("incididunt")}, |  | ||||||
| 		{key: "/foo/bar/baz/b", value: []byte("ut")}, |  | ||||||
| 		{key: "/foo/bar/baz/c", value: []byte("labore")}, |  | ||||||
| 		{key: "/foo/bar/baz/d", value: []byte("et")}, |  | ||||||
| 		// a duplicate just in case there's any edge cases |  | ||||||
| 		{key: "/foo/a", value: []byte("lorem")}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test Store |  | ||||||
| 	for _, d := range testdata { |  | ||||||
| 		if err := s.Store(d.key, d.value); err != nil { |  | ||||||
| 			t.Fatal(err.Error()) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test Load |  | ||||||
| 	for _, d := range testdata { |  | ||||||
| 		if value, err := s.Load(d.key); err != nil { |  | ||||||
| 			t.Fatal(err.Error()) |  | ||||||
| 		} else { |  | ||||||
| 			if !reflect.DeepEqual(value, d.value) { |  | ||||||
| 				t.Fatalf("Load %s: expected %v, got %v", d.key, d.value, value) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test Exists |  | ||||||
| 	for _, d := range testdata { |  | ||||||
| 		if !s.Exists(d.key) { |  | ||||||
| 			t.Fatalf("%s should exist, but doesn't\n", d.key) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test List |  | ||||||
| 	if list, err := s.List("/", true); err != nil { |  | ||||||
| 		t.Fatal(err.Error()) |  | ||||||
| 	} else { |  | ||||||
| 		var expected []string |  | ||||||
| 		for i, d := range testdata { |  | ||||||
| 			if i != len(testdata)-1 { |  | ||||||
| 				// Don't store the intentionally duplicated key |  | ||||||
| 				expected = append(expected, d.key) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		sort.Strings(expected) |  | ||||||
| 		sort.Strings(list) |  | ||||||
| 		if !reflect.DeepEqual(expected, list) { |  | ||||||
| 			t.Fatalf("List: Expected %v, got %v\n", expected, list) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if list, err := s.List("/foo", false); err != nil { |  | ||||||
| 		t.Fatal(err.Error()) |  | ||||||
| 	} else { |  | ||||||
| 		sort.Strings(list) |  | ||||||
| 		expected := []string{"/foo/a", "/foo/b", "/foo/bar", "/foo/c", "/foo/d"} |  | ||||||
| 		if !reflect.DeepEqual(expected, list) { |  | ||||||
| 			t.Fatalf("List: expected %s, got %s\n", expected, list) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test Stat |  | ||||||
| 	for _, d := range testdata { |  | ||||||
| 		info, err := s.Stat(d.key) |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatal(err.Error()) |  | ||||||
| 		} else { |  | ||||||
| 			if info.Key != d.key { |  | ||||||
| 				t.Fatalf("Stat().Key: expected %s, got %s\n", d.key, info.Key) |  | ||||||
| 			} |  | ||||||
| 			if info.Size != int64(len(d.value)) { |  | ||||||
| 				t.Fatalf("Stat().Size: expected %d, got %d\n", len(d.value), info.Size) |  | ||||||
| 			} |  | ||||||
| 			if time.Since(info.Modified) > time.Minute { |  | ||||||
| 				t.Fatalf("Stat().Modified: expected time since last modified to be < 1 minute, got %v\n", time.Since(info.Modified)) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test Delete |  | ||||||
| 	for _, d := range testdata { |  | ||||||
| 		if err := s.Delete(d.key); err != nil { |  | ||||||
| 			t.Fatal(err.Error()) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// New interface doesn't return an error, so call it in case any log.Fatal |  | ||||||
| 	// happens |  | ||||||
| 	New(acme.Cache(s)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Full test with a real zone, with  against LE staging |  | ||||||
| func TestE2e(t *testing.T) { |  | ||||||
| 	apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID") |  | ||||||
| 	kvID := os.Getenv("KV_NAMESPACE_ID") |  | ||||||
| 	if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 { |  | ||||||
| 		t.Skip("No Cloudflare API keys available, skipping test") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	testLock := memory.NewLock() |  | ||||||
| 	testStore := cfstore.NewStore( |  | ||||||
| 		cfstore.Token(apiToken), |  | ||||||
| 		cfstore.Account(accountID), |  | ||||||
| 		cfstore.Namespace(kvID), |  | ||||||
| 	) |  | ||||||
| 	testStorage := NewStorage(testLock, testStore) |  | ||||||
|  |  | ||||||
| 	conf := cloudflare.NewDefaultConfig() |  | ||||||
| 	conf.AuthToken = apiToken |  | ||||||
| 	conf.ZoneToken = apiToken |  | ||||||
| 	testChallengeProvider, err := cloudflare.NewDNSProviderConfig(conf) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err.Error()) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	testProvider := New( |  | ||||||
| 		acme.AcceptToS(true), |  | ||||||
| 		acme.Cache(testStorage), |  | ||||||
| 		acme.CA(acme.LetsEncryptStagingCA), |  | ||||||
| 		acme.ChallengeProvider(testChallengeProvider), |  | ||||||
| 		acme.OnDemand(false), |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	listener, err := testProvider.NewListener("*.micro.mu", "micro.mu") |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err.Error()) |  | ||||||
| 	} |  | ||||||
| 	go http.Serve(listener, http.NotFoundHandler()) |  | ||||||
| 	time.Sleep(10 * time.Minute) |  | ||||||
| } |  | ||||||
| @@ -9,9 +9,9 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/mholt/certmagic" | 	"github.com/caddyserver/certmagic" | ||||||
| 	"github.com/micro/go-micro/store" | 	"github.com/micro/go-micro/v2/store" | ||||||
| 	"github.com/micro/go-micro/sync/lock" | 	"github.com/micro/go-micro/v2/sync" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // File represents a "File" that will be stored in store.Store - the contents and last modified time | // File represents a "File" that will be stored in store.Store - the contents and last modified time | ||||||
| @@ -26,16 +26,16 @@ type File struct { | |||||||
| // As certmagic storage expects a filesystem (with stat() abilities) we have to implement | // As certmagic storage expects a filesystem (with stat() abilities) we have to implement | ||||||
| // the bare minimum of metadata. | // the bare minimum of metadata. | ||||||
| type storage struct { | type storage struct { | ||||||
| 	lock  lock.Lock | 	lock  sync.Sync | ||||||
| 	store store.Store | 	store store.Store | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *storage) Lock(key string) error { | func (s *storage) Lock(key string) error { | ||||||
| 	return s.lock.Acquire(key, lock.TTL(10*time.Minute)) | 	return s.lock.Lock(key, sync.LockTTL(10*time.Minute)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *storage) Unlock(key string) error { | func (s *storage) Unlock(key string) error { | ||||||
| 	return s.lock.Release(key) | 	return s.lock.Unlock(key) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *storage) Store(key string, value []byte) error { | func (s *storage) Store(key string, value []byte) error { | ||||||
| @@ -88,16 +88,16 @@ func (s *storage) Exists(key string) bool { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (s *storage) List(prefix string, recursive bool) ([]string, error) { | func (s *storage) List(prefix string, recursive bool) ([]string, error) { | ||||||
| 	records, err := s.store.List() | 	keys, err := s.store.List() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	//nolint:prealloc | 	//nolint:prealloc | ||||||
| 	var results []string | 	var results []string | ||||||
| 	for _, r := range records { | 	for _, k := range keys { | ||||||
| 		if strings.HasPrefix(r.Key, prefix) { | 		if strings.HasPrefix(k, prefix) { | ||||||
| 			results = append(results, r.Key) | 			results = append(results, k) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if recursive { | 	if recursive { | ||||||
| @@ -139,7 +139,7 @@ func (s *storage) Stat(key string) (certmagic.KeyInfo, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // NewStorage returns a certmagic.Storage backed by a go-micro/lock and go-micro/store | // NewStorage returns a certmagic.Storage backed by a go-micro/lock and go-micro/store | ||||||
| func NewStorage(lock lock.Lock, store store.Store) certmagic.Storage { | func NewStorage(lock sync.Sync, store store.Store) certmagic.Storage { | ||||||
| 	return &storage{ | 	return &storage{ | ||||||
| 		lock:  lock, | 		lock:  lock, | ||||||
| 		store: store, | 		store: store, | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								api/server/cors/cors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								api/server/cors/cors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | package cors | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // CombinedCORSHandler wraps a server and provides CORS headers | ||||||
|  | func CombinedCORSHandler(h http.Handler) http.Handler { | ||||||
|  | 	return corsHandler{h} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type corsHandler struct { | ||||||
|  | 	handler http.Handler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c corsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	SetHeaders(w, r) | ||||||
|  |  | ||||||
|  | 	if r.Method == "OPTIONS" { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c.handler.ServeHTTP(w, r) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetHeaders sets the CORS headers | ||||||
|  | func SetHeaders(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	set := func(w http.ResponseWriter, k, v string) { | ||||||
|  | 		if v := w.Header().Get(k); len(v) > 0 { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		w.Header().Set(k, v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if origin := r.Header.Get("Origin"); len(origin) > 0 { | ||||||
|  | 		set(w, "Access-Control-Allow-Origin", origin) | ||||||
|  | 	} else { | ||||||
|  | 		set(w, "Access-Control-Allow-Origin", "*") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	set(w, "Access-Control-Allow-Credentials", "true") | ||||||
|  | 	set(w, "Access-Control-Allow-Methods", "POST, PATCH, GET, OPTIONS, PUT, DELETE") | ||||||
|  | 	set(w, "Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") | ||||||
|  | } | ||||||
| @@ -9,8 +9,9 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/gorilla/handlers" | 	"github.com/gorilla/handlers" | ||||||
| 	"github.com/micro/go-micro/api/server" | 	"github.com/micro/go-micro/v2/api/server" | ||||||
| 	"github.com/micro/go-micro/util/log" | 	"github.com/micro/go-micro/v2/api/server/cors" | ||||||
|  | 	"github.com/micro/go-micro/v2/logger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type httpServer struct { | type httpServer struct { | ||||||
| @@ -22,9 +23,14 @@ type httpServer struct { | |||||||
| 	exit    chan chan error | 	exit    chan chan error | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewServer(address string) server.Server { | func NewServer(address string, opts ...server.Option) server.Server { | ||||||
|  | 	var options server.Options | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return &httpServer{ | 	return &httpServer{ | ||||||
| 		opts:    server.Options{}, | 		opts:    options, | ||||||
| 		mux:     http.NewServeMux(), | 		mux:     http.NewServeMux(), | ||||||
| 		address: address, | 		address: address, | ||||||
| 		exit:    make(chan chan error), | 		exit:    make(chan chan error), | ||||||
| @@ -45,7 +51,22 @@ func (s *httpServer) Init(opts ...server.Option) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (s *httpServer) Handle(path string, handler http.Handler) { | func (s *httpServer) Handle(path string, handler http.Handler) { | ||||||
| 	s.mux.Handle(path, handlers.CombinedLoggingHandler(os.Stdout, handler)) | 	// TODO: move this stuff out to one place with ServeHTTP | ||||||
|  |  | ||||||
|  | 	// apply the wrappers, e.g. auth | ||||||
|  | 	for _, wrapper := range s.opts.Wrappers { | ||||||
|  | 		handler = wrapper(handler) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// wrap with cors | ||||||
|  | 	if s.opts.EnableCORS { | ||||||
|  | 		handler = cors.CombinedCORSHandler(handler) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// wrap with logger | ||||||
|  | 	handler = handlers.CombinedLoggingHandler(os.Stdout, handler) | ||||||
|  |  | ||||||
|  | 	s.mux.Handle(path, handler) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *httpServer) Start() error { | func (s *httpServer) Start() error { | ||||||
| @@ -54,7 +75,7 @@ func (s *httpServer) Start() error { | |||||||
|  |  | ||||||
| 	if s.opts.EnableACME && s.opts.ACMEProvider != nil { | 	if s.opts.EnableACME && s.opts.ACMEProvider != nil { | ||||||
| 		// should we check the address to make sure its using :443? | 		// should we check the address to make sure its using :443? | ||||||
| 		l, err = s.opts.ACMEProvider.NewListener(s.opts.ACMEHosts...) | 		l, err = s.opts.ACMEProvider.Listen(s.opts.ACMEHosts...) | ||||||
| 	} else if s.opts.EnableTLS && s.opts.TLSConfig != nil { | 	} else if s.opts.EnableTLS && s.opts.TLSConfig != nil { | ||||||
| 		l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig) | 		l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig) | ||||||
| 	} else { | 	} else { | ||||||
| @@ -65,7 +86,9 @@ func (s *httpServer) Start() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Logf("HTTP API Listening on %s", l.Addr().String()) | 	if logger.V(logger.InfoLevel, logger.DefaultLogger) { | ||||||
|  | 		logger.Infof("HTTP API Listening on %s", l.Addr().String()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	s.mtx.Lock() | 	s.mtx.Lock() | ||||||
| 	s.address = l.Addr().String() | 	s.address = l.Addr().String() | ||||||
| @@ -74,7 +97,7 @@ func (s *httpServer) Start() error { | |||||||
| 	go func() { | 	go func() { | ||||||
| 		if err := http.Serve(l, s.mux); err != nil { | 		if err := http.Serve(l, s.mux); err != nil { | ||||||
| 			// temporary fix | 			// temporary fix | ||||||
| 			//log.Fatal(err) | 			//logger.Fatal(err) | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,18 +2,37 @@ package server | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/api/server/acme" | 	"github.com/micro/go-micro/v2/api/resolver" | ||||||
|  | 	"github.com/micro/go-micro/v2/api/server/acme" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Option func(o *Options) | type Option func(o *Options) | ||||||
|  |  | ||||||
| type Options struct { | type Options struct { | ||||||
| 	EnableACME   bool | 	EnableACME   bool | ||||||
|  | 	EnableCORS   bool | ||||||
| 	ACMEProvider acme.Provider | 	ACMEProvider acme.Provider | ||||||
| 	EnableTLS    bool | 	EnableTLS    bool | ||||||
| 	ACMEHosts    []string | 	ACMEHosts    []string | ||||||
| 	TLSConfig    *tls.Config | 	TLSConfig    *tls.Config | ||||||
|  | 	Resolver     resolver.Resolver | ||||||
|  | 	Wrappers     []Wrapper | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Wrapper func(h http.Handler) http.Handler | ||||||
|  |  | ||||||
|  | func WrapHandler(w Wrapper) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Wrappers = append(o.Wrappers, w) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func EnableCORS(b bool) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.EnableCORS = b | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func EnableACME(b bool) Option { | func EnableACME(b bool) Option { | ||||||
| @@ -45,3 +64,9 @@ func TLSConfig(t *tls.Config) Option { | |||||||
| 		o.TLSConfig = t | 		o.TLSConfig = t | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func Resolver(r resolver.Resolver) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Resolver = r | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										268
									
								
								api/service/proto/api.pb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								api/service/proto/api.pb.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | |||||||
|  | // Code generated by protoc-gen-go. DO NOT EDIT. | ||||||
|  | // source: api/service/proto/api.proto | ||||||
|  |  | ||||||
|  | package go_micro_api | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	context "context" | ||||||
|  | 	fmt "fmt" | ||||||
|  | 	proto "github.com/golang/protobuf/proto" | ||||||
|  | 	grpc "google.golang.org/grpc" | ||||||
|  | 	codes "google.golang.org/grpc/codes" | ||||||
|  | 	status "google.golang.org/grpc/status" | ||||||
|  | 	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.ProtoPackageIsVersion3 // please upgrade the proto package | ||||||
|  |  | ||||||
|  | type Endpoint struct { | ||||||
|  | 	Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` | ||||||
|  | 	Host                 []string `protobuf:"bytes,2,rep,name=host,proto3" json:"host,omitempty"` | ||||||
|  | 	Path                 []string `protobuf:"bytes,3,rep,name=path,proto3" json:"path,omitempty"` | ||||||
|  | 	Method               []string `protobuf:"bytes,4,rep,name=method,proto3" json:"method,omitempty"` | ||||||
|  | 	Stream               bool     `protobuf:"varint,5,opt,name=stream,proto3" json:"stream,omitempty"` | ||||||
|  | 	XXX_NoUnkeyedLiteral struct{} `json:"-"` | ||||||
|  | 	XXX_unrecognized     []byte   `json:"-"` | ||||||
|  | 	XXX_sizecache        int32    `json:"-"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *Endpoint) Reset()         { *m = Endpoint{} } | ||||||
|  | func (m *Endpoint) String() string { return proto.CompactTextString(m) } | ||||||
|  | func (*Endpoint) ProtoMessage()    {} | ||||||
|  | func (*Endpoint) Descriptor() ([]byte, []int) { | ||||||
|  | 	return fileDescriptor_c4a48b6b680b5c31, []int{0} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *Endpoint) XXX_Unmarshal(b []byte) error { | ||||||
|  | 	return xxx_messageInfo_Endpoint.Unmarshal(m, b) | ||||||
|  | } | ||||||
|  | func (m *Endpoint) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | ||||||
|  | 	return xxx_messageInfo_Endpoint.Marshal(b, m, deterministic) | ||||||
|  | } | ||||||
|  | func (m *Endpoint) XXX_Merge(src proto.Message) { | ||||||
|  | 	xxx_messageInfo_Endpoint.Merge(m, src) | ||||||
|  | } | ||||||
|  | func (m *Endpoint) XXX_Size() int { | ||||||
|  | 	return xxx_messageInfo_Endpoint.Size(m) | ||||||
|  | } | ||||||
|  | func (m *Endpoint) XXX_DiscardUnknown() { | ||||||
|  | 	xxx_messageInfo_Endpoint.DiscardUnknown(m) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var xxx_messageInfo_Endpoint proto.InternalMessageInfo | ||||||
|  |  | ||||||
|  | func (m *Endpoint) GetName() string { | ||||||
|  | 	if m != nil { | ||||||
|  | 		return m.Name | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *Endpoint) GetHost() []string { | ||||||
|  | 	if m != nil { | ||||||
|  | 		return m.Host | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *Endpoint) GetPath() []string { | ||||||
|  | 	if m != nil { | ||||||
|  | 		return m.Path | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *Endpoint) GetMethod() []string { | ||||||
|  | 	if m != nil { | ||||||
|  | 		return m.Method | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *Endpoint) GetStream() bool { | ||||||
|  | 	if m != nil { | ||||||
|  | 		return m.Stream | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type EmptyResponse struct { | ||||||
|  | 	XXX_NoUnkeyedLiteral struct{} `json:"-"` | ||||||
|  | 	XXX_unrecognized     []byte   `json:"-"` | ||||||
|  | 	XXX_sizecache        int32    `json:"-"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *EmptyResponse) Reset()         { *m = EmptyResponse{} } | ||||||
|  | func (m *EmptyResponse) String() string { return proto.CompactTextString(m) } | ||||||
|  | func (*EmptyResponse) ProtoMessage()    {} | ||||||
|  | func (*EmptyResponse) Descriptor() ([]byte, []int) { | ||||||
|  | 	return fileDescriptor_c4a48b6b680b5c31, []int{1} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *EmptyResponse) XXX_Unmarshal(b []byte) error { | ||||||
|  | 	return xxx_messageInfo_EmptyResponse.Unmarshal(m, b) | ||||||
|  | } | ||||||
|  | func (m *EmptyResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | ||||||
|  | 	return xxx_messageInfo_EmptyResponse.Marshal(b, m, deterministic) | ||||||
|  | } | ||||||
|  | func (m *EmptyResponse) XXX_Merge(src proto.Message) { | ||||||
|  | 	xxx_messageInfo_EmptyResponse.Merge(m, src) | ||||||
|  | } | ||||||
|  | func (m *EmptyResponse) XXX_Size() int { | ||||||
|  | 	return xxx_messageInfo_EmptyResponse.Size(m) | ||||||
|  | } | ||||||
|  | func (m *EmptyResponse) XXX_DiscardUnknown() { | ||||||
|  | 	xxx_messageInfo_EmptyResponse.DiscardUnknown(m) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var xxx_messageInfo_EmptyResponse proto.InternalMessageInfo | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	proto.RegisterType((*Endpoint)(nil), "go.micro.api.Endpoint") | ||||||
|  | 	proto.RegisterType((*EmptyResponse)(nil), "go.micro.api.EmptyResponse") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { proto.RegisterFile("api/service/proto/api.proto", fileDescriptor_c4a48b6b680b5c31) } | ||||||
|  |  | ||||||
|  | var fileDescriptor_c4a48b6b680b5c31 = []byte{ | ||||||
|  | 	// 212 bytes of a gzipped FileDescriptorProto | ||||||
|  | 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0xd0, 0xc1, 0x4a, 0x03, 0x31, | ||||||
|  | 	0x10, 0x80, 0x61, 0xd7, 0xad, 0x65, 0x1d, 0x14, 0x21, 0x87, 0x12, 0xec, 0x65, 0xd9, 0x53, 0x4f, | ||||||
|  | 	0x59, 0xd0, 0x27, 0x28, 0xda, 0x17, 0xd8, 0x37, 0x88, 0xed, 0xd0, 0x9d, 0x43, 0x32, 0x43, 0x32, | ||||||
|  | 	0x14, 0x7c, 0x08, 0xdf, 0x59, 0x12, 0x2b, 0x2c, 0x5e, 0xbd, 0xfd, 0xf3, 0x1d, 0x86, 0x61, 0x60, | ||||||
|  | 	0xeb, 0x85, 0xc6, 0x8c, 0xe9, 0x42, 0x47, 0x1c, 0x25, 0xb1, 0xf2, 0xe8, 0x85, 0x5c, 0x2d, 0xf3, | ||||||
|  | 	0x70, 0x66, 0x17, 0xe8, 0x98, 0xd8, 0x79, 0xa1, 0xe1, 0x02, 0xdd, 0x21, 0x9e, 0x84, 0x29, 0xaa, | ||||||
|  | 	0x31, 0xb0, 0x8a, 0x3e, 0xa0, 0x6d, 0xfa, 0x66, 0x77, 0x3f, 0xd5, 0x2e, 0x36, 0x73, 0x56, 0x7b, | ||||||
|  | 	0xdb, 0xb7, 0xc5, 0x4a, 0x17, 0x13, 0xaf, 0xb3, 0x6d, 0x7f, 0xac, 0xb4, 0xd9, 0xc0, 0x3a, 0xa0, | ||||||
|  | 	0xce, 0x7c, 0xb2, 0xab, 0xaa, 0xd7, 0xa9, 0x78, 0xd6, 0x84, 0x3e, 0xd8, 0xbb, 0xbe, 0xd9, 0x75, | ||||||
|  | 	0xd3, 0x75, 0x1a, 0x9e, 0xe0, 0xf1, 0x10, 0x44, 0x3f, 0x27, 0xcc, 0xc2, 0x31, 0xe3, 0xcb, 0x57, | ||||||
|  | 	0x03, 0xed, 0x5e, 0xc8, 0xec, 0xa1, 0x9b, 0xf0, 0x4c, 0x59, 0x31, 0x99, 0x8d, 0x5b, 0xde, 0xea, | ||||||
|  | 	0x7e, 0x0f, 0x7d, 0xde, 0xfe, 0xf1, 0xe5, 0xa2, 0xe1, 0xc6, 0xbc, 0x01, 0xbc, 0x63, 0xfa, 0xdf, | ||||||
|  | 	0x92, 0x8f, 0x75, 0xfd, 0xd6, 0xeb, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x46, 0x62, 0x67, 0x30, | ||||||
|  | 	0x4c, 0x01, 0x00, 0x00, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Reference imports to suppress errors if they are not otherwise used. | ||||||
|  | var _ context.Context | ||||||
|  | var _ grpc.ClientConn | ||||||
|  |  | ||||||
|  | // This is a compile-time assertion to ensure that this generated file | ||||||
|  | // is compatible with the grpc package it is being compiled against. | ||||||
|  | const _ = grpc.SupportPackageIsVersion4 | ||||||
|  |  | ||||||
|  | // ApiClient is the client API for Api service. | ||||||
|  | // | ||||||
|  | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. | ||||||
|  | type ApiClient interface { | ||||||
|  | 	Register(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error) | ||||||
|  | 	Deregister(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type apiClient struct { | ||||||
|  | 	cc *grpc.ClientConn | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewApiClient(cc *grpc.ClientConn) ApiClient { | ||||||
|  | 	return &apiClient{cc} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *apiClient) Register(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error) { | ||||||
|  | 	out := new(EmptyResponse) | ||||||
|  | 	err := c.cc.Invoke(ctx, "/go.micro.api.Api/Register", in, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *apiClient) Deregister(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error) { | ||||||
|  | 	out := new(EmptyResponse) | ||||||
|  | 	err := c.cc.Invoke(ctx, "/go.micro.api.Api/Deregister", in, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ApiServer is the server API for Api service. | ||||||
|  | type ApiServer interface { | ||||||
|  | 	Register(context.Context, *Endpoint) (*EmptyResponse, error) | ||||||
|  | 	Deregister(context.Context, *Endpoint) (*EmptyResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnimplementedApiServer can be embedded to have forward compatible implementations. | ||||||
|  | type UnimplementedApiServer struct { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (*UnimplementedApiServer) Register(ctx context.Context, req *Endpoint) (*EmptyResponse, error) { | ||||||
|  | 	return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") | ||||||
|  | } | ||||||
|  | func (*UnimplementedApiServer) Deregister(ctx context.Context, req *Endpoint) (*EmptyResponse, error) { | ||||||
|  | 	return nil, status.Errorf(codes.Unimplemented, "method Deregister not implemented") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterApiServer(s *grpc.Server, srv ApiServer) { | ||||||
|  | 	s.RegisterService(&_Api_serviceDesc, srv) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func _Api_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | ||||||
|  | 	in := new(Endpoint) | ||||||
|  | 	if err := dec(in); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if interceptor == nil { | ||||||
|  | 		return srv.(ApiServer).Register(ctx, in) | ||||||
|  | 	} | ||||||
|  | 	info := &grpc.UnaryServerInfo{ | ||||||
|  | 		Server:     srv, | ||||||
|  | 		FullMethod: "/go.micro.api.Api/Register", | ||||||
|  | 	} | ||||||
|  | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) { | ||||||
|  | 		return srv.(ApiServer).Register(ctx, req.(*Endpoint)) | ||||||
|  | 	} | ||||||
|  | 	return interceptor(ctx, in, info, handler) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func _Api_Deregister_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | ||||||
|  | 	in := new(Endpoint) | ||||||
|  | 	if err := dec(in); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if interceptor == nil { | ||||||
|  | 		return srv.(ApiServer).Deregister(ctx, in) | ||||||
|  | 	} | ||||||
|  | 	info := &grpc.UnaryServerInfo{ | ||||||
|  | 		Server:     srv, | ||||||
|  | 		FullMethod: "/go.micro.api.Api/Deregister", | ||||||
|  | 	} | ||||||
|  | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) { | ||||||
|  | 		return srv.(ApiServer).Deregister(ctx, req.(*Endpoint)) | ||||||
|  | 	} | ||||||
|  | 	return interceptor(ctx, in, info, handler) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _Api_serviceDesc = grpc.ServiceDesc{ | ||||||
|  | 	ServiceName: "go.micro.api.Api", | ||||||
|  | 	HandlerType: (*ApiServer)(nil), | ||||||
|  | 	Methods: []grpc.MethodDesc{ | ||||||
|  | 		{ | ||||||
|  | 			MethodName: "Register", | ||||||
|  | 			Handler:    _Api_Register_Handler, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			MethodName: "Deregister", | ||||||
|  | 			Handler:    _Api_Deregister_Handler, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	Streams:  []grpc.StreamDesc{}, | ||||||
|  | 	Metadata: "api/service/proto/api.proto", | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								api/service/proto/api.pb.micro.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								api/service/proto/api.pb.micro.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | // Code generated by protoc-gen-micro. DO NOT EDIT. | ||||||
|  | // source: api/service/proto/api.proto | ||||||
|  |  | ||||||
|  | package go_micro_api | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	fmt "fmt" | ||||||
|  | 	proto "github.com/golang/protobuf/proto" | ||||||
|  | 	math "math" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	context "context" | ||||||
|  | 	api "github.com/micro/go-micro/v2/api" | ||||||
|  | 	client "github.com/micro/go-micro/v2/client" | ||||||
|  | 	server "github.com/micro/go-micro/v2/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.ProtoPackageIsVersion3 // please upgrade the proto package | ||||||
|  |  | ||||||
|  | // Reference imports to suppress errors if they are not otherwise used. | ||||||
|  | var _ api.Endpoint | ||||||
|  | var _ context.Context | ||||||
|  | var _ client.Option | ||||||
|  | var _ server.Option | ||||||
|  |  | ||||||
|  | // Api Endpoints for Api service | ||||||
|  |  | ||||||
|  | func NewApiEndpoints() []*api.Endpoint { | ||||||
|  | 	return []*api.Endpoint{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Client API for Api service | ||||||
|  |  | ||||||
|  | type ApiService interface { | ||||||
|  | 	Register(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error) | ||||||
|  | 	Deregister(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type apiService struct { | ||||||
|  | 	c    client.Client | ||||||
|  | 	name string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewApiService(name string, c client.Client) ApiService { | ||||||
|  | 	return &apiService{ | ||||||
|  | 		c:    c, | ||||||
|  | 		name: name, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *apiService) Register(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Api.Register", in) | ||||||
|  | 	out := new(EmptyResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *apiService) Deregister(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Api.Deregister", in) | ||||||
|  | 	out := new(EmptyResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Server API for Api service | ||||||
|  |  | ||||||
|  | type ApiHandler interface { | ||||||
|  | 	Register(context.Context, *Endpoint, *EmptyResponse) error | ||||||
|  | 	Deregister(context.Context, *Endpoint, *EmptyResponse) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterApiHandler(s server.Server, hdlr ApiHandler, opts ...server.HandlerOption) error { | ||||||
|  | 	type api interface { | ||||||
|  | 		Register(ctx context.Context, in *Endpoint, out *EmptyResponse) error | ||||||
|  | 		Deregister(ctx context.Context, in *Endpoint, out *EmptyResponse) error | ||||||
|  | 	} | ||||||
|  | 	type Api struct { | ||||||
|  | 		api | ||||||
|  | 	} | ||||||
|  | 	h := &apiHandler{hdlr} | ||||||
|  | 	return s.Handle(s.NewHandler(&Api{h}, opts...)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type apiHandler struct { | ||||||
|  | 	ApiHandler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *apiHandler) Register(ctx context.Context, in *Endpoint, out *EmptyResponse) error { | ||||||
|  | 	return h.ApiHandler.Register(ctx, in, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *apiHandler) Deregister(ctx context.Context, in *Endpoint, out *EmptyResponse) error { | ||||||
|  | 	return h.ApiHandler.Deregister(ctx, in, out) | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								api/service/proto/api.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/service/proto/api.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | package go.micro.api; | ||||||
|  |  | ||||||
|  | service Api { | ||||||
|  |   rpc Register(Endpoint) returns (EmptyResponse) {}; | ||||||
|  |   rpc Deregister(Endpoint) returns (EmptyResponse) {}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message Endpoint { | ||||||
|  |   string name = 1; | ||||||
|  |   repeated string host = 2; | ||||||
|  |   repeated string path = 3; | ||||||
|  |   repeated string method = 4; | ||||||
|  |   bool stream = 5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message EmptyResponse {} | ||||||
							
								
								
									
										125
									
								
								auth/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								auth/auth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | |||||||
|  | // Package auth provides authentication and authorization capability | ||||||
|  | package auth | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// ErrNotFound is returned when a resouce cannot be found | ||||||
|  | 	ErrNotFound = errors.New("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") | ||||||
|  | 	// ErrInvalidRole is returned when the role provided was invalid | ||||||
|  | 	ErrInvalidRole = errors.New("invalid role") | ||||||
|  | 	// ErrForbidden is returned when a user does not have the necessary roles to access a resource | ||||||
|  | 	ErrForbidden = errors.New("resource forbidden") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Auth providers 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) | ||||||
|  | 	// Grant access to a resource | ||||||
|  | 	Grant(role string, res *Resource) error | ||||||
|  | 	// Revoke access to a resource | ||||||
|  | 	Revoke(role string, res *Resource) error | ||||||
|  | 	// Verify an account has access to a resource | ||||||
|  | 	Verify(acc *Account, res *Resource) error | ||||||
|  | 	// Inspect a token | ||||||
|  | 	Inspect(token string) (*Account, error) | ||||||
|  | 	// Token generated using refresh token | ||||||
|  | 	Token(opts ...TokenOption) (*Token, error) | ||||||
|  | 	// String returns the name of the implementation | ||||||
|  | 	String() string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Resource is an entity such as a user or | ||||||
|  | type Resource struct { | ||||||
|  | 	// Name of the resource | ||||||
|  | 	Name string `json:"name"` | ||||||
|  | 	// Type of resource, e.g. | ||||||
|  | 	Type string `json:"type"` | ||||||
|  | 	// Endpoint resource e.g NotesService.Create | ||||||
|  | 	Endpoint string `json:"endpoint"` | ||||||
|  | 	// Namespace the resource belongs to | ||||||
|  | 	Namespace string `json:"namespace"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Account provided by an auth provider | ||||||
|  | type Account struct { | ||||||
|  | 	// ID of the account e.g. email | ||||||
|  | 	ID string `json:"id"` | ||||||
|  | 	// Type of the account, e.g. service | ||||||
|  | 	Type string `json:"type"` | ||||||
|  | 	// Provider who issued the account | ||||||
|  | 	Provider string `json:"provider"` | ||||||
|  | 	// Roles associated with the Account | ||||||
|  | 	Roles []string `json:"roles"` | ||||||
|  | 	// Any other associated metadata | ||||||
|  | 	Metadata map[string]string `json:"metadata"` | ||||||
|  | 	// Namespace the account belongs to | ||||||
|  | 	Namespace string `json:"namespace"` | ||||||
|  | 	// Secret for the account, e.g. the password | ||||||
|  | 	Secret string `json:"secret"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HasRole returns a boolean indicating if the account has the given role | ||||||
|  | func (a *Account) HasRole(role string) bool { | ||||||
|  | 	if a.Roles == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, r := range a.Roles { | ||||||
|  | 		if r == role { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Token can be short or long lived | ||||||
|  | type Token struct { | ||||||
|  | 	// 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"` | ||||||
|  | 	// Time of token creation | ||||||
|  | 	Created time.Time `json:"created"` | ||||||
|  | 	// Time of token expiry | ||||||
|  | 	Expiry time.Time `json:"expiry"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// DefaultNamespace used for auth | ||||||
|  | 	DefaultNamespace = "go.micro" | ||||||
|  | 	// TokenCookieName is the name of the cookie which stores the auth token | ||||||
|  | 	TokenCookieName = "micro-token" | ||||||
|  | 	// BearerScheme used for Authorization header | ||||||
|  | 	BearerScheme = "Bearer " | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								auth/auth_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								auth/auth_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | package auth | ||||||
|  |  | ||||||
|  | import "testing" | ||||||
|  |  | ||||||
|  | func TestHasRole(t *testing.T) { | ||||||
|  | 	if new(Account).HasRole("foo") { | ||||||
|  | 		t.Errorf("Expected the blank account to not have a role") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	acc := Account{Roles: []string{"foo"}} | ||||||
|  | 	if !acc.HasRole("foo") { | ||||||
|  | 		t.Errorf("Expected the account to have the foo role") | ||||||
|  | 	} | ||||||
|  | 	if acc.HasRole("bar") { | ||||||
|  | 		t.Errorf("Expected the account to not have the bar role") | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								auth/default.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								auth/default.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | package auth | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/provider/basic" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	DefaultAuth = NewAuth() | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func NewAuth(opts ...Option) Auth { | ||||||
|  | 	options := Options{ | ||||||
|  | 		Provider: basic.NewProvider(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &noop{ | ||||||
|  | 		opts: options, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type noop struct { | ||||||
|  | 	opts Options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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, | ||||||
|  | 		Roles:     options.Roles, | ||||||
|  | 		Secret:    options.Secret, | ||||||
|  | 		Metadata:  options.Metadata, | ||||||
|  | 		Namespace: DefaultNamespace, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Grant access to a resource | ||||||
|  | func (n *noop) Grant(role string, res *Resource) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Revoke access to a resource | ||||||
|  | func (n *noop) Revoke(role string, res *Resource) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Verify an account has access to a resource | ||||||
|  | func (n *noop) Verify(acc *Account, res *Resource) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Inspect a token | ||||||
|  | func (n *noop) Inspect(token string) (*Account, error) { | ||||||
|  | 	return &Account{ | ||||||
|  | 		ID:        uuid.New().String(), | ||||||
|  | 		Namespace: DefaultNamespace, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Token generation using an account id and secret | ||||||
|  | func (n *noop) Token(opts ...TokenOption) (*Token, error) { | ||||||
|  | 	return &Token{}, nil | ||||||
|  | } | ||||||
							
								
								
									
										196
									
								
								auth/jwt/jwt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								auth/jwt/jwt.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | |||||||
|  | package jwt | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/auth" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/token" | ||||||
|  | 	jwtToken "github.com/micro/go-micro/v2/auth/token/jwt" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // NewAuth returns a new instance of the Auth service | ||||||
|  | func NewAuth(opts ...auth.Option) auth.Auth { | ||||||
|  | 	j := new(jwt) | ||||||
|  | 	j.Init(opts...) | ||||||
|  | 	return j | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type rule struct { | ||||||
|  | 	role     string | ||||||
|  | 	resource *auth.Resource | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type jwt struct { | ||||||
|  | 	options auth.Options | ||||||
|  | 	jwt     token.Provider | ||||||
|  | 	rules   []*rule | ||||||
|  |  | ||||||
|  | 	sync.Mutex | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(j.options.Namespace) == 0 { | ||||||
|  | 		j.options.Namespace = auth.DefaultNamespace | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	j.jwt = jwtToken.NewTokenProvider( | ||||||
|  | 		token.WithPrivateKey(j.options.PrivateKey), | ||||||
|  | 		token.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, | ||||||
|  | 		Roles:     options.Roles, | ||||||
|  | 		Provider:  options.Provider, | ||||||
|  | 		Metadata:  options.Metadata, | ||||||
|  | 		Namespace: 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 *jwt) Grant(role string, res *auth.Resource) error { | ||||||
|  | 	j.Lock() | ||||||
|  | 	defer j.Unlock() | ||||||
|  | 	j.rules = append(j.rules, &rule{role, res}) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j *jwt) Revoke(role string, res *auth.Resource) error { | ||||||
|  | 	j.Lock() | ||||||
|  | 	defer j.Unlock() | ||||||
|  |  | ||||||
|  | 	rules := make([]*rule, 0, len(j.rules)) | ||||||
|  |  | ||||||
|  | 	var ruleFound bool | ||||||
|  | 	for _, r := range rules { | ||||||
|  | 		if r.role == role && r.resource == res { | ||||||
|  | 			ruleFound = true | ||||||
|  | 		} else { | ||||||
|  | 			rules = append(rules, r) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !ruleFound { | ||||||
|  | 		return auth.ErrNotFound | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	j.rules = rules | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j *jwt) Verify(acc *auth.Account, res *auth.Resource) error { | ||||||
|  | 	j.Lock() | ||||||
|  | 	if len(res.Namespace) == 0 { | ||||||
|  | 		res.Namespace = j.options.Namespace | ||||||
|  | 	} | ||||||
|  | 	rules := j.rules | ||||||
|  | 	j.Unlock() | ||||||
|  |  | ||||||
|  | 	for _, rule := range rules { | ||||||
|  | 		// validate the rule applies to the requested resource | ||||||
|  | 		if rule.resource.Namespace != "*" && rule.resource.Namespace != res.Namespace { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if rule.resource.Type != "*" && rule.resource.Type != res.Type { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if rule.resource.Name != "*" && rule.resource.Name != res.Name { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if rule.resource.Endpoint != "*" && rule.resource.Endpoint != res.Endpoint { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// a blank role indicates anyone can access the resource, even without an account | ||||||
|  | 		if rule.role == "" { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// all furter checks require an account | ||||||
|  | 		if acc == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// this rule allows any account access, allow the request | ||||||
|  | 		if rule.role == "*" { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// if the account has the necessary role, allow the request | ||||||
|  | 		for _, r := range acc.Roles { | ||||||
|  | 			if r == rule.role { | ||||||
|  | 				return nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// no rules matched, forbid the request | ||||||
|  | 	return auth.ErrForbidden | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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, token.WithExpiry(options.Expiry)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	refresh, err := j.jwt.Generate(account, token.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 | ||||||
|  | } | ||||||
							
								
								
									
										238
									
								
								auth/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								auth/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,238 @@ | |||||||
|  | package auth | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/provider" | ||||||
|  | 	"github.com/micro/go-micro/v2/client" | ||||||
|  | 	"github.com/micro/go-micro/v2/store" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func NewOptions(opts ...Option) Options { | ||||||
|  | 	var options Options | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  | 	if len(options.Namespace) == 0 { | ||||||
|  | 		options.Namespace = DefaultNamespace | ||||||
|  | 	} | ||||||
|  | 	if options.Client == nil { | ||||||
|  | 		options.Client = client.DefaultClient | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Options struct { | ||||||
|  | 	// Namespace the service belongs to | ||||||
|  | 	Namespace string | ||||||
|  | 	// ID is the services auth ID | ||||||
|  | 	ID string | ||||||
|  | 	// Secret is used to authenticate the service | ||||||
|  | 	Secret string | ||||||
|  | 	// Token is the services token used to authenticate itself | ||||||
|  | 	Token *Token | ||||||
|  | 	// PublicKey for decoding JWTs | ||||||
|  | 	PublicKey string | ||||||
|  | 	// PrivateKey for encoding JWTs | ||||||
|  | 	PrivateKey string | ||||||
|  | 	// Provider is an auth provider | ||||||
|  | 	Provider provider.Provider | ||||||
|  | 	// LoginURL is the relative url path where a user can login | ||||||
|  | 	LoginURL string | ||||||
|  | 	// Store to back auth | ||||||
|  | 	Store store.Store | ||||||
|  | 	// Client to use for RPC | ||||||
|  | 	Client client.Client | ||||||
|  | 	// 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 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Store to back auth | ||||||
|  | func Store(s store.Store) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Store = s | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Provider set the auth provider | ||||||
|  | func Provider(p provider.Provider) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Provider = p | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LoginURL sets the auth LoginURL | ||||||
|  | func LoginURL(url string) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.LoginURL = url | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithClient sets the client to use when making requests | ||||||
|  | func WithClient(c client.Client) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Client = c | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GenerateOptions struct { | ||||||
|  | 	// Metadata associated with the account | ||||||
|  | 	Metadata map[string]string | ||||||
|  | 	// Roles/scopes associated with the account | ||||||
|  | 	Roles []string | ||||||
|  | 	// Namespace the account belongs too | ||||||
|  | 	Namespace 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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithRoles for the generated account | ||||||
|  | func WithRoles(rs ...string) GenerateOption { | ||||||
|  | 	return func(o *GenerateOptions) { | ||||||
|  | 		o.Roles = rs | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithNamespace for the generated account | ||||||
|  | func WithNamespace(n string) GenerateOption { | ||||||
|  | 	return func(o *GenerateOptions) { | ||||||
|  | 		o.Namespace = n | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithProvider for the generated account | ||||||
|  | func WithProvider(p string) GenerateOption { | ||||||
|  | 	return func(o *GenerateOptions) { | ||||||
|  | 		o.Provider = p | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 defualt expiry of token | ||||||
|  | 	if options.Expiry == 0 { | ||||||
|  | 		options.Expiry = time.Minute | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return options | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								auth/provider/basic/basic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								auth/provider/basic/basic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | package basic | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/provider" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // NewProvider returns an initialised basic provider | ||||||
|  | func NewProvider(opts ...provider.Option) provider.Provider { | ||||||
|  | 	var options provider.Options | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  | 	return &basic{options} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type basic struct { | ||||||
|  | 	opts provider.Options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *basic) String() string { | ||||||
|  | 	return "basic" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *basic) Options() provider.Options { | ||||||
|  | 	return b.opts | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *basic) Endpoint(...provider.EndpointOption) string { | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *basic) Redirect() string { | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								auth/provider/oauth/oauth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								auth/provider/oauth/oauth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | package oauth | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/provider" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // NewProvider returns an initialised oauth provider | ||||||
|  | func NewProvider(opts ...provider.Option) provider.Provider { | ||||||
|  | 	var options provider.Options | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  | 	return &oauth{options} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type oauth struct { | ||||||
|  | 	opts provider.Options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *oauth) String() string { | ||||||
|  | 	return "oauth" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *oauth) Options() provider.Options { | ||||||
|  | 	return o.opts | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *oauth) Endpoint(opts ...provider.EndpointOption) string { | ||||||
|  | 	var options provider.EndpointOptions | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	params := make(url.Values) | ||||||
|  | 	params.Add("response_type", "code") | ||||||
|  |  | ||||||
|  | 	if len(options.State) > 0 { | ||||||
|  | 		params.Add("state", options.State) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(options.LoginHint) > 0 { | ||||||
|  | 		params.Add("login_hint", options.LoginHint) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if clientID := o.opts.ClientID; len(clientID) > 0 { | ||||||
|  | 		params.Add("client_id", clientID) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if scope := o.opts.Scope; len(scope) > 0 { | ||||||
|  | 		params.Add("scope", scope) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if redir := o.Redirect(); len(redir) > 0 { | ||||||
|  | 		params.Add("redirect_uri", redir) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fmt.Sprintf("%v?%v", o.opts.Endpoint, params.Encode()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *oauth) Redirect() string { | ||||||
|  | 	return o.opts.Redirect | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								auth/provider/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								auth/provider/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | package provider | ||||||
|  |  | ||||||
|  | // Option returns a function which sets an option | ||||||
|  | type Option func(*Options) | ||||||
|  |  | ||||||
|  | // Options a provider can have | ||||||
|  | type Options struct { | ||||||
|  | 	// ClientID is the application's ID. | ||||||
|  | 	ClientID string | ||||||
|  | 	// ClientSecret is the application's secret. | ||||||
|  | 	ClientSecret string | ||||||
|  | 	// Endpoint for the provider | ||||||
|  | 	Endpoint string | ||||||
|  | 	// Redirect url incase of UI | ||||||
|  | 	Redirect string | ||||||
|  | 	// Scope of the oauth request | ||||||
|  | 	Scope string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Credentials is an option which sets the client id and secret | ||||||
|  | func Credentials(id, secret string) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.ClientID = id | ||||||
|  | 		o.ClientSecret = secret | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Endpoint sets the endpoint option | ||||||
|  | func Endpoint(e string) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Endpoint = e | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Redirect sets the Redirect option | ||||||
|  | func Redirect(r string) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Redirect = r | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Scope sets the oauth scope | ||||||
|  | func Scope(s string) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Scope = s | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								auth/provider/provider.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								auth/provider/provider.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | // Package provider is an external auth provider e.g oauth | ||||||
|  | package provider | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Provider is an auth provider | ||||||
|  | type Provider interface { | ||||||
|  | 	// String returns the name of the provider | ||||||
|  | 	String() string | ||||||
|  | 	// Options returns the options of a provider | ||||||
|  | 	Options() Options | ||||||
|  | 	// Endpoint for the provider | ||||||
|  | 	Endpoint(...EndpointOption) string | ||||||
|  | 	// Redirect url incase of UI | ||||||
|  | 	Redirect() string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Grant is a granted authorisation | ||||||
|  | type Grant struct { | ||||||
|  | 	// token for reuse | ||||||
|  | 	Token string | ||||||
|  | 	// Expiry of the token | ||||||
|  | 	Expiry time.Time | ||||||
|  | 	// Scopes associated with grant | ||||||
|  | 	Scopes []string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type EndpointOptions struct { | ||||||
|  | 	// State is a code to verify the req | ||||||
|  | 	State string | ||||||
|  | 	// LoginHint prefils the user id on oauth clients | ||||||
|  | 	LoginHint string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type EndpointOption func(*EndpointOptions) | ||||||
|  |  | ||||||
|  | func WithState(c string) EndpointOption { | ||||||
|  | 	return func(o *EndpointOptions) { | ||||||
|  | 		o.State = c | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func WithLoginHint(hint string) EndpointOption { | ||||||
|  | 	return func(o *EndpointOptions) { | ||||||
|  | 		o.LoginHint = hint | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										1586
									
								
								auth/service/proto/auth.pb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1586
									
								
								auth/service/proto/auth.pb.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										279
									
								
								auth/service/proto/auth.pb.micro.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								auth/service/proto/auth.pb.micro.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | |||||||
|  | // Code generated by protoc-gen-micro. DO NOT EDIT. | ||||||
|  | // source: auth/service/proto/auth.proto | ||||||
|  |  | ||||||
|  | package go_micro_auth | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	fmt "fmt" | ||||||
|  | 	proto "github.com/golang/protobuf/proto" | ||||||
|  | 	math "math" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	context "context" | ||||||
|  | 	api "github.com/micro/go-micro/v2/api" | ||||||
|  | 	client "github.com/micro/go-micro/v2/client" | ||||||
|  | 	server "github.com/micro/go-micro/v2/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.ProtoPackageIsVersion3 // please upgrade the proto package | ||||||
|  |  | ||||||
|  | // Reference imports to suppress errors if they are not otherwise used. | ||||||
|  | var _ api.Endpoint | ||||||
|  | var _ context.Context | ||||||
|  | var _ client.Option | ||||||
|  | var _ server.Option | ||||||
|  |  | ||||||
|  | // Api Endpoints for Auth service | ||||||
|  |  | ||||||
|  | func NewAuthEndpoints() []*api.Endpoint { | ||||||
|  | 	return []*api.Endpoint{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Client API for Auth service | ||||||
|  |  | ||||||
|  | type AuthService interface { | ||||||
|  | 	Generate(ctx context.Context, in *GenerateRequest, opts ...client.CallOption) (*GenerateResponse, error) | ||||||
|  | 	Inspect(ctx context.Context, in *InspectRequest, opts ...client.CallOption) (*InspectResponse, error) | ||||||
|  | 	Token(ctx context.Context, in *TokenRequest, opts ...client.CallOption) (*TokenResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type authService struct { | ||||||
|  | 	c    client.Client | ||||||
|  | 	name string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewAuthService(name string, c client.Client) AuthService { | ||||||
|  | 	return &authService{ | ||||||
|  | 		c:    c, | ||||||
|  | 		name: name, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *authService) Generate(ctx context.Context, in *GenerateRequest, opts ...client.CallOption) (*GenerateResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Auth.Generate", in) | ||||||
|  | 	out := new(GenerateResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *authService) Inspect(ctx context.Context, in *InspectRequest, opts ...client.CallOption) (*InspectResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Auth.Inspect", in) | ||||||
|  | 	out := new(InspectResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *authService) Token(ctx context.Context, in *TokenRequest, opts ...client.CallOption) (*TokenResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Auth.Token", in) | ||||||
|  | 	out := new(TokenResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Server API for Auth service | ||||||
|  |  | ||||||
|  | type AuthHandler interface { | ||||||
|  | 	Generate(context.Context, *GenerateRequest, *GenerateResponse) error | ||||||
|  | 	Inspect(context.Context, *InspectRequest, *InspectResponse) error | ||||||
|  | 	Token(context.Context, *TokenRequest, *TokenResponse) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterAuthHandler(s server.Server, hdlr AuthHandler, opts ...server.HandlerOption) error { | ||||||
|  | 	type auth interface { | ||||||
|  | 		Generate(ctx context.Context, in *GenerateRequest, out *GenerateResponse) error | ||||||
|  | 		Inspect(ctx context.Context, in *InspectRequest, out *InspectResponse) error | ||||||
|  | 		Token(ctx context.Context, in *TokenRequest, out *TokenResponse) error | ||||||
|  | 	} | ||||||
|  | 	type Auth struct { | ||||||
|  | 		auth | ||||||
|  | 	} | ||||||
|  | 	h := &authHandler{hdlr} | ||||||
|  | 	return s.Handle(s.NewHandler(&Auth{h}, opts...)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type authHandler struct { | ||||||
|  | 	AuthHandler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *authHandler) Generate(ctx context.Context, in *GenerateRequest, out *GenerateResponse) error { | ||||||
|  | 	return h.AuthHandler.Generate(ctx, in, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *authHandler) Inspect(ctx context.Context, in *InspectRequest, out *InspectResponse) error { | ||||||
|  | 	return h.AuthHandler.Inspect(ctx, in, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *authHandler) Token(ctx context.Context, in *TokenRequest, out *TokenResponse) error { | ||||||
|  | 	return h.AuthHandler.Token(ctx, in, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Api Endpoints for Accounts service | ||||||
|  |  | ||||||
|  | func NewAccountsEndpoints() []*api.Endpoint { | ||||||
|  | 	return []*api.Endpoint{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Client API for Accounts service | ||||||
|  |  | ||||||
|  | type AccountsService interface { | ||||||
|  | 	List(ctx context.Context, in *ListAccountsRequest, opts ...client.CallOption) (*ListAccountsResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type accountsService struct { | ||||||
|  | 	c    client.Client | ||||||
|  | 	name string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewAccountsService(name string, c client.Client) AccountsService { | ||||||
|  | 	return &accountsService{ | ||||||
|  | 		c:    c, | ||||||
|  | 		name: name, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *accountsService) List(ctx context.Context, in *ListAccountsRequest, opts ...client.CallOption) (*ListAccountsResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Accounts.List", in) | ||||||
|  | 	out := new(ListAccountsResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Server API for Accounts service | ||||||
|  |  | ||||||
|  | type AccountsHandler interface { | ||||||
|  | 	List(context.Context, *ListAccountsRequest, *ListAccountsResponse) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterAccountsHandler(s server.Server, hdlr AccountsHandler, opts ...server.HandlerOption) error { | ||||||
|  | 	type accounts interface { | ||||||
|  | 		List(ctx context.Context, in *ListAccountsRequest, out *ListAccountsResponse) error | ||||||
|  | 	} | ||||||
|  | 	type Accounts struct { | ||||||
|  | 		accounts | ||||||
|  | 	} | ||||||
|  | 	h := &accountsHandler{hdlr} | ||||||
|  | 	return s.Handle(s.NewHandler(&Accounts{h}, opts...)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type accountsHandler struct { | ||||||
|  | 	AccountsHandler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *accountsHandler) List(ctx context.Context, in *ListAccountsRequest, out *ListAccountsResponse) error { | ||||||
|  | 	return h.AccountsHandler.List(ctx, in, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Api Endpoints for Rules service | ||||||
|  |  | ||||||
|  | func NewRulesEndpoints() []*api.Endpoint { | ||||||
|  | 	return []*api.Endpoint{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Client API for Rules service | ||||||
|  |  | ||||||
|  | type RulesService interface { | ||||||
|  | 	Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) | ||||||
|  | 	Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) | ||||||
|  | 	List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type rulesService struct { | ||||||
|  | 	c    client.Client | ||||||
|  | 	name string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewRulesService(name string, c client.Client) RulesService { | ||||||
|  | 	return &rulesService{ | ||||||
|  | 		c:    c, | ||||||
|  | 		name: name, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *rulesService) Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Rules.Create", in) | ||||||
|  | 	out := new(CreateResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *rulesService) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Rules.Delete", in) | ||||||
|  | 	out := new(DeleteResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *rulesService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) { | ||||||
|  | 	req := c.c.NewRequest(c.name, "Rules.List", in) | ||||||
|  | 	out := new(ListResponse) | ||||||
|  | 	err := c.c.Call(ctx, req, out, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return out, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Server API for Rules service | ||||||
|  |  | ||||||
|  | type RulesHandler interface { | ||||||
|  | 	Create(context.Context, *CreateRequest, *CreateResponse) error | ||||||
|  | 	Delete(context.Context, *DeleteRequest, *DeleteResponse) error | ||||||
|  | 	List(context.Context, *ListRequest, *ListResponse) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterRulesHandler(s server.Server, hdlr RulesHandler, opts ...server.HandlerOption) error { | ||||||
|  | 	type rules interface { | ||||||
|  | 		Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error | ||||||
|  | 		Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error | ||||||
|  | 		List(ctx context.Context, in *ListRequest, out *ListResponse) error | ||||||
|  | 	} | ||||||
|  | 	type Rules struct { | ||||||
|  | 		rules | ||||||
|  | 	} | ||||||
|  | 	h := &rulesHandler{hdlr} | ||||||
|  | 	return s.Handle(s.NewHandler(&Rules{h}, opts...)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type rulesHandler struct { | ||||||
|  | 	RulesHandler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *rulesHandler) Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error { | ||||||
|  | 	return h.RulesHandler.Create(ctx, in, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *rulesHandler) Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error { | ||||||
|  | 	return h.RulesHandler.Delete(ctx, in, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *rulesHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error { | ||||||
|  | 	return h.RulesHandler.List(ctx, in, out) | ||||||
|  | } | ||||||
							
								
								
									
										136
									
								
								auth/service/proto/auth.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								auth/service/proto/auth.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | package go.micro.auth; | ||||||
|  |  | ||||||
|  | service Auth { | ||||||
|  | 	rpc Generate(GenerateRequest) returns (GenerateResponse) {}; | ||||||
|  | 	rpc Inspect(InspectRequest) returns (InspectResponse) {};		 | ||||||
|  | 	rpc Token(TokenRequest) returns (TokenResponse) {}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | service Accounts { | ||||||
|  | 	rpc List(ListAccountsRequest) returns (ListAccountsResponse) {}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | service Rules { | ||||||
|  | 	rpc Create(CreateRequest) returns (CreateResponse) {}; | ||||||
|  | 	rpc Delete(DeleteRequest) returns (DeleteResponse) {}; | ||||||
|  | 	rpc List(ListRequest) returns (ListResponse) {}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message ListAccountsRequest { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message ListAccountsResponse { | ||||||
|  | 	repeated Account accounts = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message Token { | ||||||
|  | 	string access_token = 1; | ||||||
|  | 	string refresh_token = 2; | ||||||
|  | 	int64 created = 3; | ||||||
|  | 	int64 expiry = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message Account { | ||||||
|  | 	string id = 1; | ||||||
|  | 	string type = 2; | ||||||
|  | 	repeated string roles = 3; | ||||||
|  | 	map<string, string> metadata = 4; | ||||||
|  | 	string namespace = 5; | ||||||
|  | 	string provider = 6; | ||||||
|  | 	string secret = 7; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message Resource{ | ||||||
|  | 	string name = 1; | ||||||
|  | 	string type = 2; | ||||||
|  | 	string endpoint = 3; | ||||||
|  | 	string namespace = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message GenerateRequest { | ||||||
|  | 	string id = 1; | ||||||
|  | 	repeated string roles = 2; | ||||||
|  | 	map<string, string> metadata = 3; | ||||||
|  | 	string namespace = 4; | ||||||
|  | 	string secret = 5; | ||||||
|  | 	string type = 6; | ||||||
|  | 	string provider = 7; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message GenerateResponse { | ||||||
|  | 	Account account = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message GrantRequest { | ||||||
|  | 	string role = 1; | ||||||
|  | 	Resource resource = 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message GrantResponse {} | ||||||
|  |  | ||||||
|  | message RevokeRequest { | ||||||
|  | 	string role = 1; | ||||||
|  | 	Resource resource = 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message RevokeResponse {} | ||||||
|  |  | ||||||
|  | message InspectRequest { | ||||||
|  | 	string token = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message InspectResponse { | ||||||
|  | 	Account account = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message TokenRequest { | ||||||
|  | 	string id = 1; | ||||||
|  | 	string secret = 2; | ||||||
|  | 	string refresh_token = 3; | ||||||
|  | 	int64 token_expiry = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message TokenResponse { | ||||||
|  | 	Token token = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | enum Access { | ||||||
|  | 	UNKNOWN = 0; | ||||||
|  | 	GRANTED = 1; | ||||||
|  | 	DENIED = 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message Rule { | ||||||
|  | 	string id = 1; | ||||||
|  | 	string role = 2; | ||||||
|  | 	Resource resource = 3; | ||||||
|  | 	Access access = 4; | ||||||
|  | 	int32 priority = 5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message CreateRequest { | ||||||
|  | 	string role = 1; | ||||||
|  | 	Resource resource = 2; | ||||||
|  | 	Access access = 3; | ||||||
|  | 	int32 priority = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message CreateResponse {} | ||||||
|  |  | ||||||
|  | message DeleteRequest { | ||||||
|  | 	string role = 1; | ||||||
|  | 	Resource resource = 2; | ||||||
|  | 	Access access = 3; | ||||||
|  | 	int32 priority = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message DeleteResponse {} | ||||||
|  |  | ||||||
|  | message ListRequest { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message ListResponse { | ||||||
|  | 	repeated Rule rules = 1; | ||||||
|  | } | ||||||
							
								
								
									
										331
									
								
								auth/service/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								auth/service/service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,331 @@ | |||||||
|  | package service | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/auth" | ||||||
|  | 	pb "github.com/micro/go-micro/v2/auth/service/proto" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/token" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/token/jwt" | ||||||
|  | 	"github.com/micro/go-micro/v2/client" | ||||||
|  | 	log "github.com/micro/go-micro/v2/logger" | ||||||
|  | 	"github.com/micro/go-micro/v2/util/jitter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // svc is the service implementation of the Auth interface | ||||||
|  | type svc struct { | ||||||
|  | 	options auth.Options | ||||||
|  | 	auth    pb.AuthService | ||||||
|  | 	rule    pb.RulesService | ||||||
|  | 	jwt     token.Provider | ||||||
|  |  | ||||||
|  | 	rules []*pb.Rule | ||||||
|  | 	sync.Mutex | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *svc) String() string { | ||||||
|  | 	return "service" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *svc) Init(opts ...auth.Option) { | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&s.options) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if s.options.Client == nil { | ||||||
|  | 		s.options.Client = client.DefaultClient | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	s.auth = pb.NewAuthService("go.micro.auth", s.options.Client) | ||||||
|  | 	s.rule = pb.NewRulesService("go.micro.auth", s.options.Client) | ||||||
|  |  | ||||||
|  | 	// if we have a JWT public key passed as an option, | ||||||
|  | 	// we can decode tokens with the type "JWT" locally | ||||||
|  | 	// and not have to make an RPC call | ||||||
|  | 	if key := s.options.PublicKey; len(key) > 0 { | ||||||
|  | 		s.jwt = jwt.NewTokenProvider(token.WithPublicKey(key)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *svc) Options() auth.Options { | ||||||
|  | 	s.Lock() | ||||||
|  | 	defer s.Unlock() | ||||||
|  | 	return s.options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Generate a new account | ||||||
|  | func (s *svc) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) { | ||||||
|  | 	options := auth.NewGenerateOptions(opts...) | ||||||
|  |  | ||||||
|  | 	rsp, err := s.auth.Generate(context.TODO(), &pb.GenerateRequest{ | ||||||
|  | 		Id:        id, | ||||||
|  | 		Type:      options.Type, | ||||||
|  | 		Secret:    options.Secret, | ||||||
|  | 		Roles:     options.Roles, | ||||||
|  | 		Metadata:  options.Metadata, | ||||||
|  | 		Provider:  options.Provider, | ||||||
|  | 		Namespace: options.Namespace, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return serializeAccount(rsp.Account), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Grant access to a resource | ||||||
|  | func (s *svc) Grant(role string, res *auth.Resource) error { | ||||||
|  | 	_, err := s.rule.Create(context.TODO(), &pb.CreateRequest{ | ||||||
|  | 		Role:   role, | ||||||
|  | 		Access: pb.Access_GRANTED, | ||||||
|  | 		Resource: &pb.Resource{ | ||||||
|  | 			Namespace: res.Namespace, | ||||||
|  | 			Type:      res.Type, | ||||||
|  | 			Name:      res.Name, | ||||||
|  | 			Endpoint:  res.Endpoint, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Revoke access to a resource | ||||||
|  | func (s *svc) Revoke(role string, res *auth.Resource) error { | ||||||
|  | 	_, err := s.rule.Delete(context.TODO(), &pb.DeleteRequest{ | ||||||
|  | 		Role:   role, | ||||||
|  | 		Access: pb.Access_GRANTED, | ||||||
|  | 		Resource: &pb.Resource{ | ||||||
|  | 			Namespace: res.Namespace, | ||||||
|  | 			Type:      res.Type, | ||||||
|  | 			Name:      res.Name, | ||||||
|  | 			Endpoint:  res.Endpoint, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Verify an account has access to a resource | ||||||
|  | func (s *svc) Verify(acc *auth.Account, res *auth.Resource) error { | ||||||
|  | 	// load the rules if none are loaded | ||||||
|  | 	s.loadRulesIfEmpty() | ||||||
|  |  | ||||||
|  | 	// set the namespace on the resource | ||||||
|  | 	if len(res.Namespace) == 0 { | ||||||
|  | 		res.Namespace = s.Options().Namespace | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	queries := [][]string{ | ||||||
|  | 		{res.Namespace, res.Type, res.Name, res.Endpoint}, // check for specific role, e.g. service.foo.ListFoo:admin (role is checked in accessForRule) | ||||||
|  | 		{res.Namespace, res.Type, res.Name, "*"},          // check for wildcard endpoint, e.g. service.foo* | ||||||
|  | 		{res.Namespace, res.Type, "*"},                    // check for wildcard name, e.g. service.* | ||||||
|  | 		{res.Namespace, "*"},                              // check for wildcard type, e.g. * | ||||||
|  | 		{"*"},                                             // check for wildcard namespace | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// endpoint is a url which can have wildcard excludes, e.g. | ||||||
|  | 	// "/foo/*" will allow "/foo/bar" | ||||||
|  | 	if comps := strings.Split(res.Endpoint, "/"); len(comps) > 1 { | ||||||
|  | 		for i := 1; i < len(comps); i++ { | ||||||
|  | 			wildcard := fmt.Sprintf("%v/*", strings.Join(comps[0:i], "/")) | ||||||
|  | 			queries = append(queries, []string{res.Type, res.Name, wildcard}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// set a default account id / namespace to log | ||||||
|  | 	logID := acc.ID | ||||||
|  | 	if len(logID) == 0 { | ||||||
|  | 		logID = "[no account]" | ||||||
|  | 	} | ||||||
|  | 	logNamespace := acc.Namespace | ||||||
|  | 	if len(logNamespace) == 0 { | ||||||
|  | 		logNamespace = "[no namespace]" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, q := range queries { | ||||||
|  | 		for _, rule := range s.listRules(q...) { | ||||||
|  | 			switch accessForRule(rule, acc, res) { | ||||||
|  | 			case pb.Access_UNKNOWN: | ||||||
|  | 				continue // rule did not specify access, check the next rule | ||||||
|  | 			case pb.Access_GRANTED: | ||||||
|  | 				log.Tracef("%v:%v granted access to %v:%v:%v:%v by rule %v", logNamespace, logID, res.Namespace, res.Type, res.Name, res.Endpoint, rule.Id) | ||||||
|  | 				return nil // rule grants the account access to the resource | ||||||
|  | 			case pb.Access_DENIED: | ||||||
|  | 				log.Tracef("%v:%v denied access to %v:%v:%v:%v by rule %v", logNamespace, logID, res.Namespace, res.Type, res.Name, res.Endpoint, rule.Id) | ||||||
|  | 				return auth.ErrForbidden // rule denies access to the resource | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// no rules were found for the resource, default to denying access | ||||||
|  | 	log.Tracef("%v:%v denied access to %v:%v:%v:%v by lack of rule (%v rules found for namespace)", logNamespace, logID, res.Namespace, res.Type, res.Name, res.Endpoint, len(s.listRules(res.Namespace))) | ||||||
|  | 	return auth.ErrForbidden | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Inspect a token | ||||||
|  | func (s *svc) Inspect(token string) (*auth.Account, error) { | ||||||
|  | 	// try to decode JWT locally and fall back to srv if an error occurs | ||||||
|  | 	if len(strings.Split(token, ".")) == 3 && s.jwt != nil { | ||||||
|  | 		return s.jwt.Inspect(token) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// the token is not a JWT or we do not have the keys to decode it, | ||||||
|  | 	// fall back to the auth service | ||||||
|  | 	rsp, err := s.auth.Inspect(context.TODO(), &pb.InspectRequest{Token: token}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return serializeAccount(rsp.Account), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Token generation using an account ID and secret | ||||||
|  | func (s *svc) Token(opts ...auth.TokenOption) (*auth.Token, error) { | ||||||
|  | 	options := auth.NewTokenOptions(opts...) | ||||||
|  |  | ||||||
|  | 	rsp, err := s.auth.Token(context.Background(), &pb.TokenRequest{ | ||||||
|  | 		Id:           options.ID, | ||||||
|  | 		Secret:       options.Secret, | ||||||
|  | 		RefreshToken: options.RefreshToken, | ||||||
|  | 		TokenExpiry:  int64(options.Expiry.Seconds()), | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return serializeToken(rsp.Token), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ruleJoinKey = ":" | ||||||
|  |  | ||||||
|  | // accessForRule returns a rule status, indicating if a rule permits access to a | ||||||
|  | // resource for a given account | ||||||
|  | func accessForRule(rule *pb.Rule, acc *auth.Account, res *auth.Resource) pb.Access { | ||||||
|  | 	// a blank role permits access to the public | ||||||
|  | 	if rule.Role == "" { | ||||||
|  | 		return rule.Access | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// a * role permits access to any user | ||||||
|  | 	if rule.Role == "*" && acc != nil { | ||||||
|  | 		return rule.Access | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, role := range acc.Roles { | ||||||
|  | 		if rule.Role == role { | ||||||
|  | 			return rule.Access | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// allow user.anything if role is user.* | ||||||
|  | 		if strings.HasSuffix(rule.Role, ".*") && strings.HasPrefix(rule.Role, role+".") { | ||||||
|  | 			return rule.Access | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return pb.Access_UNKNOWN | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // listRules gets all the rules from the store which match the filters. | ||||||
|  | // filters are namespace, type, name and then endpoint. | ||||||
|  | func (s *svc) listRules(filters ...string) []*pb.Rule { | ||||||
|  | 	s.Lock() | ||||||
|  | 	defer s.Unlock() | ||||||
|  |  | ||||||
|  | 	var rules []*pb.Rule | ||||||
|  | 	for _, r := range s.rules { | ||||||
|  | 		if len(filters) > 0 && r.Resource.Namespace != filters[0] { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if len(filters) > 1 && r.Resource.Type != filters[1] { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if len(filters) > 2 && r.Resource.Name != filters[2] { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if len(filters) > 3 && r.Resource.Endpoint != filters[3] { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		rules = append(rules, r) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// sort rules by priority | ||||||
|  | 	sort.Slice(rules, func(i, j int) bool { | ||||||
|  | 		return rules[i].Priority < rules[j].Priority | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return rules | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // loadRules retrieves the rules from the auth service | ||||||
|  | func (s *svc) loadRules() { | ||||||
|  | 	rsp, err := s.rule.List(context.TODO(), &pb.ListRequest{}) | ||||||
|  | 	s.Lock() | ||||||
|  | 	defer s.Unlock() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Errorf("Error listing rules: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	s.rules = rsp.Rules | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *svc) loadRulesIfEmpty() { | ||||||
|  | 	s.Lock() | ||||||
|  | 	rules := s.rules | ||||||
|  | 	s.Unlock() | ||||||
|  |  | ||||||
|  | 	if len(rules) == 0 { | ||||||
|  | 		s.loadRules() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serializeToken(t *pb.Token) *auth.Token { | ||||||
|  | 	return &auth.Token{ | ||||||
|  | 		AccessToken:  t.AccessToken, | ||||||
|  | 		RefreshToken: t.RefreshToken, | ||||||
|  | 		Created:      time.Unix(t.Created, 0), | ||||||
|  | 		Expiry:       time.Unix(t.Expiry, 0), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serializeAccount(a *pb.Account) *auth.Account { | ||||||
|  | 	return &auth.Account{ | ||||||
|  | 		ID:        a.Id, | ||||||
|  | 		Roles:     a.Roles, | ||||||
|  | 		Secret:    a.Secret, | ||||||
|  | 		Metadata:  a.Metadata, | ||||||
|  | 		Provider:  a.Provider, | ||||||
|  | 		Namespace: a.Namespace, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewAuth returns a new instance of the Auth service | ||||||
|  | func NewAuth(opts ...auth.Option) auth.Auth { | ||||||
|  | 	options := auth.NewOptions(opts...) | ||||||
|  | 	if options.Client == nil { | ||||||
|  | 		options.Client = client.DefaultClient | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	service := &svc{ | ||||||
|  | 		auth:    pb.NewAuthService("go.micro.auth", options.Client), | ||||||
|  | 		rule:    pb.NewRulesService("go.micro.auth", options.Client), | ||||||
|  | 		options: options, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// load rules periodically from the auth service | ||||||
|  | 	go func() { | ||||||
|  | 		ruleTimer := time.NewTicker(time.Second * 30) | ||||||
|  |  | ||||||
|  | 		for { | ||||||
|  | 			<-ruleTimer.C | ||||||
|  | 			time.Sleep(jitter.Do(time.Second * 5)) | ||||||
|  | 			service.loadRules() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	return service | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								auth/service/sevice_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								auth/service/sevice_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | package service | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	pb "github.com/micro/go-micro/v2/auth/service/proto" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestListRulesSorting(t *testing.T) { | ||||||
|  | 	s := &svc{ | ||||||
|  | 		rules: []*pb.Rule{ | ||||||
|  | 			&pb.Rule{Priority: 1}, | ||||||
|  | 			&pb.Rule{Priority: 3}, | ||||||
|  | 			&pb.Rule{Priority: 2}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var priorities []int32 | ||||||
|  | 	for _, r := range s.listRules() { | ||||||
|  | 		priorities = append(priorities, r.Priority) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if priorities[0] != 1 || priorities[1] != 2 || priorities[2] != 3 { | ||||||
|  | 		t.Errorf("Incorrect Rule Sequence") | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										89
									
								
								auth/token/basic/basic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								auth/token/basic/basic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | package basic | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/token" | ||||||
|  | 	"github.com/micro/go-micro/v2/store" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Basic implementation of token provider, backed by the store | ||||||
|  | type Basic struct { | ||||||
|  | 	store store.Store | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// StorePrefix to isolate tokens | ||||||
|  | 	StorePrefix = "tokens/" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // NewTokenProvider returns an initialized basic provider | ||||||
|  | func NewTokenProvider(opts ...token.Option) token.Provider { | ||||||
|  | 	options := token.NewOptions(opts...) | ||||||
|  |  | ||||||
|  | 	if options.Store == nil { | ||||||
|  | 		options.Store = store.DefaultStore | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &Basic{ | ||||||
|  | 		store: options.Store, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Generate a token for an account | ||||||
|  | func (b *Basic) Generate(acc *auth.Account, opts ...token.GenerateOption) (*token.Token, error) { | ||||||
|  | 	options := token.NewGenerateOptions(opts...) | ||||||
|  |  | ||||||
|  | 	// marshal the account to bytes | ||||||
|  | 	bytes, err := json.Marshal(acc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// write to the store | ||||||
|  | 	key := uuid.New().String() | ||||||
|  | 	err = b.store.Write(&store.Record{ | ||||||
|  | 		Key:    fmt.Sprintf("%v%v", StorePrefix, key), | ||||||
|  | 		Value:  bytes, | ||||||
|  | 		Expiry: options.Expiry, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// return the token | ||||||
|  | 	return &token.Token{ | ||||||
|  | 		Token:   key, | ||||||
|  | 		Created: time.Now(), | ||||||
|  | 		Expiry:  time.Now().Add(options.Expiry), | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Inspect a token | ||||||
|  | func (b *Basic) Inspect(t string) (*auth.Account, error) { | ||||||
|  | 	// lookup the token in the store | ||||||
|  | 	recs, err := b.store.Read(StorePrefix + t) | ||||||
|  | 	if err == store.ErrNotFound { | ||||||
|  | 		return nil, token.ErrInvalidToken | ||||||
|  | 	} else if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	bytes := recs[0].Value | ||||||
|  |  | ||||||
|  | 	// unmarshal the bytes | ||||||
|  | 	var acc *auth.Account | ||||||
|  | 	if err := json.Unmarshal(bytes, &acc); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return acc, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // String returns basic | ||||||
|  | func (b *Basic) String() string { | ||||||
|  | 	return "basic" | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								auth/token/basic/basic_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								auth/token/basic/basic_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | package basic | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/auth" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/token" | ||||||
|  | 	"github.com/micro/go-micro/v2/store/memory" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestGenerate(t *testing.T) { | ||||||
|  | 	store := memory.NewStore() | ||||||
|  | 	b := NewTokenProvider(token.WithStore(store)) | ||||||
|  |  | ||||||
|  | 	_, err := b.Generate(&auth.Account{ID: "test"}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Generate returned %v error, expected nil", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	recs, err := store.List() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unable to read from store: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if len(recs) != 1 { | ||||||
|  | 		t.Errorf("Generate didn't write to the store, expected 1 record, got %v", len(recs)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestInspect(t *testing.T) { | ||||||
|  | 	store := memory.NewStore() | ||||||
|  | 	b := NewTokenProvider(token.WithStore(store)) | ||||||
|  |  | ||||||
|  | 	t.Run("Valid token", func(t *testing.T) { | ||||||
|  | 		md := map[string]string{"foo": "bar"} | ||||||
|  | 		roles := []string{"admin"} | ||||||
|  | 		subject := "test" | ||||||
|  |  | ||||||
|  | 		tok, err := b.Generate(&auth.Account{ID: subject, Roles: roles, Metadata: md}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Generate returned %v error, expected nil", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		tok2, err := b.Inspect(tok.Token) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Inspect returned %v error, expected nil", err) | ||||||
|  | 		} | ||||||
|  | 		if tok2.ID != subject { | ||||||
|  | 			t.Errorf("Inspect returned %v as the token subject, expected %v", tok2.ID, subject) | ||||||
|  | 		} | ||||||
|  | 		if len(tok2.Roles) != len(roles) { | ||||||
|  | 			t.Errorf("Inspect returned %v roles, expected %v", len(tok2.Roles), len(roles)) | ||||||
|  | 		} | ||||||
|  | 		if len(tok2.Metadata) != len(md) { | ||||||
|  | 			t.Errorf("Inspect returned %v as the token metadata, expected %v", tok2.Metadata, md) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Invalid token", func(t *testing.T) { | ||||||
|  | 		_, err := b.Inspect("Invalid token") | ||||||
|  | 		if err != token.ErrInvalidToken { | ||||||
|  | 			t.Fatalf("Inspect returned %v error, expected %v", err, token.ErrInvalidToken) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										112
									
								
								auth/token/jwt/jwt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								auth/token/jwt/jwt.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | package jwt | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/dgrijalva/jwt-go" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/token" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // authClaims to be encoded in the JWT | ||||||
|  | type authClaims struct { | ||||||
|  | 	Type      string            `json:"type"` | ||||||
|  | 	Roles     []string          `json:"roles"` | ||||||
|  | 	Provider  string            `json:"provider"` | ||||||
|  | 	Metadata  map[string]string `json:"metadata"` | ||||||
|  | 	Namespace string            `json:"namespace"` | ||||||
|  |  | ||||||
|  | 	jwt.StandardClaims | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // JWT implementation of token provider | ||||||
|  | type JWT struct { | ||||||
|  | 	opts token.Options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewTokenProvider returns an initialized basic provider | ||||||
|  | func NewTokenProvider(opts ...token.Option) token.Provider { | ||||||
|  | 	return &JWT{ | ||||||
|  | 		opts: token.NewOptions(opts...), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Generate a new JWT | ||||||
|  | func (j *JWT) Generate(acc *auth.Account, opts ...token.GenerateOption) (*token.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, token.ErrEncodingToken | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// parse the options | ||||||
|  | 	options := token.NewGenerateOptions(opts...) | ||||||
|  |  | ||||||
|  | 	// generate the JWT | ||||||
|  | 	expiry := time.Now().Add(options.Expiry) | ||||||
|  | 	t := jwt.NewWithClaims(jwt.SigningMethodRS256, authClaims{ | ||||||
|  | 		acc.Type, acc.Roles, acc.Provider, acc.Metadata, acc.Namespace, jwt.StandardClaims{ | ||||||
|  | 			Subject:   acc.ID, | ||||||
|  | 			ExpiresAt: expiry.Unix(), | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	tok, err := t.SignedString(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// return the token | ||||||
|  | 	return &token.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, token.ErrInvalidToken | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// validate the token | ||||||
|  | 	if !res.Valid { | ||||||
|  | 		return nil, token.ErrInvalidToken | ||||||
|  | 	} | ||||||
|  | 	claims, ok := res.Claims.(*authClaims) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, token.ErrInvalidToken | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// return the token | ||||||
|  | 	return &auth.Account{ | ||||||
|  | 		ID:        claims.Subject, | ||||||
|  | 		Type:      claims.Type, | ||||||
|  | 		Roles:     claims.Roles, | ||||||
|  | 		Provider:  claims.Provider, | ||||||
|  | 		Metadata:  claims.Metadata, | ||||||
|  | 		Namespace: claims.Namespace, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // String returns JWT | ||||||
|  | func (j *JWT) String() string { | ||||||
|  | 	return "jwt" | ||||||
|  | } | ||||||
							
								
								
									
										87
									
								
								auth/token/jwt/jwt_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								auth/token/jwt/jwt_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | package jwt | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/auth" | ||||||
|  | 	"github.com/micro/go-micro/v2/auth/token" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestGenerate(t *testing.T) { | ||||||
|  | 	privKey, err := ioutil.ReadFile("test/sample_key") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unable to read private key: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	j := NewTokenProvider( | ||||||
|  | 		token.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 := ioutil.ReadFile("test/sample_key.pub") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unable to read public key: %v", err) | ||||||
|  | 	} | ||||||
|  | 	privKey, err := ioutil.ReadFile("test/sample_key") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unable to read private key: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	j := NewTokenProvider( | ||||||
|  | 		token.WithPublicKey(string(pubKey)), | ||||||
|  | 		token.WithPrivateKey(string(privKey)), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	t.Run("Valid token", func(t *testing.T) { | ||||||
|  | 		md := map[string]string{"foo": "bar"} | ||||||
|  | 		roles := []string{"admin"} | ||||||
|  | 		subject := "test" | ||||||
|  |  | ||||||
|  | 		acc := &auth.Account{ID: subject, Roles: roles, 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.Roles) != len(roles) { | ||||||
|  | 			t.Errorf("Inspect returned %v roles, expected %v", len(tok2.Roles), len(roles)) | ||||||
|  | 		} | ||||||
|  | 		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{}, token.WithExpiry(-10*time.Second)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Generate returned %v error, expected nil", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if _, err = j.Inspect(tok.Token); err != token.ErrInvalidToken { | ||||||
|  | 			t.Fatalf("Inspect returned %v error, expected %v", err, token.ErrInvalidToken) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Invalid token", func(t *testing.T) { | ||||||
|  | 		_, err := j.Inspect("Invalid token") | ||||||
|  | 		if err != token.ErrInvalidToken { | ||||||
|  | 			t.Fatalf("Inspect returned %v error, expected %v", err, token.ErrInvalidToken) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								auth/token/jwt/test/sample_key
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								auth/token/jwt/test/sample_key
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS3dJQkFBS0NBZ0VBOFNiSlA1WGJFaWRSbTViMnNOcExHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkCi9SbDkvMXBNVjdNaU8zTEh3dGhIQzJCUllxcisxd0Zkb1pDR0JZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUKMEJIL2xYUU1xeUVxRjVNSTJ6ZWpDNHpNenIxNU9OK2dFNEpuaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtLwptVWRJVC9MYUY3a1F4eVlLNVZLbitOZ09Xek1sektBQXBDbjdUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsCm85akRqbFk1b0JPY3pmcWVOV0hLNUdYQjdRd3BMTmg5NDZQelpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDUKd2xFcThoTmhtaG01Tk5lL08rR2dqQkROU2ZVaDA2K3E0bmdtYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1bwpSdFFoZ2lZOTEwcFBmOWJhdVhXcXdVQ1VhNHFzSHpqS1IwTC9OMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVCnJnTHJQYkVCOWVnY0drMzgrYnBLczNaNlJyNSt0bkQxQklQSUZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVUKVEdEeFV4OG9qOFZJZVJuV0RxNk1jMWlKcDhVeWNpQklUUnR3NGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMApsYVF6QXVQM2FpV1hJTXAyc2M4U2MrQmwrTGpYbUJveEJyYUJIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YCmdGS1NzSW5IRHJIVk95V1BCZTNmYWRFYzc3YituYi9leE96cjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUEKQVFLQ0FnRUFqUzc1Q2VvUlRRcUtBNzZaaFNiNGEzNVlKRENtcEpSazFsRTNKYnFzNFYxRnhXaDBjZmJYeG9VMgpSdTRRYjUrZWhsdWJGSFQ2a1BxdG9uRWhRVExjMUNmVE9WbHJOb3hocDVZM2ZyUmlQcnNnNXcwK1R3RUtrcFJUCnltanJQTXdQbGxCM2U0NmVaYmVXWGc3R3FFVmptMGcxVFRRK0tocVM4R0w3VGJlTFhRN1ZTem9ydTNCNVRKMVEKeEN6TVB0dnQ2eDYrU3JrcmhvZG1iT3VNRkpDam1TbWxmck9pZzQ4Zkc3NUpERHRObXpLWHBEUVJpYUNodFJhVQpQRHpmUTlTamhYdFFqdkZvWFFFT3BqdkZVRjR2WldNUWNQNUw1VklDM3JRSWp4MFNzQTN6S0FwakVUbjJHNjN2CktZby8zVWttbzhkUCtGRHA3NCs5a3pLNHFFaFJycEl3bEtiN0VOZWtDUXZqUFl1K3pyKzMyUXdQNTJ2L2FveWQKdjJJaUY3M2laTU1vZDhhYjJuQStyVEI2T0cvOVlSYk5kV21tay9VTi9jUHYrN214TmZ6Y1d1ZU1XcThxMXh4eAptNTNpR0NSQ29PQ1lDQk4zcUFkb1JwYW5xd3lCOUxrLzFCQjBHUld3MjgxK3VhNXNYRnZBVDBKeTVURnduMncvClU1MlJKWFlNOXVhMFBvd214b0RDUWRuNFZYVkdNZGdXaHN4aXhHRlYwOUZObWJJQWJaN0xaWGtkS1gzc1ZVbTcKWU1WYWIzVVo2bEhtdXYzT1NzcHNVUlRqN1hiRzZpaVVlaDU1aW91OENWbnRndWtFcnEzQTQwT05FVzhjNDBzOQphVTBGaSs4eWZpQTViaVZHLzF0bWlucUVERkhuQStnWk1xNEhlSkZxcWZxaEZKa1JwRGtDZ2dFQkFQeGR1NGNKCm5Da1duZDdPWFlHMVM3UDdkVWhRUzgwSDlteW9uZFc5bGFCQm84RWRPeTVTZzNOUmsxQ2pNZFZ1a3FMcjhJSnkKeStLWk15SVpvSlJvbllaMEtIUUVMR3ZLbzFOS2NLQ1FJbnYvWHVCdFJpRzBVb1pQNVkwN0RpRFBRQWpYUjlXUwpBc0EzMmQ1eEtFOC91Y3h0MjVQVzJFakNBUmtVeHQ5d0tKazN3bC9JdXVYRlExTDdDWjJsOVlFUjlHeWxUbzhNCmxXUEY3YndtUFV4UVNKaTNVS0FjTzZweTVUU1lkdWQ2aGpQeXJwSXByNU42VGpmTlRFWkVBeU9LbXVpOHVkUkoKMUg3T3RQVEhGZElKQjNrNEJnRDZtRE1HbjB2SXBLaDhZN3NtRUZBbFkvaXlCZjMvOHk5VHVMb1BycEdqR3RHbgp4Y2RpMHFud2p0SGFNbFVDZ2dFQkFQU2Z0dVFCQ2dTU2JLUSswUEFSR2VVeEQyTmlvZk1teENNTmdHUzJ5Ull3CjRGaGV4ZWkwMVJoaFk1NjE3UjduR1dzb0czd1RQa3dvRTJtbE1aQkoxeWEvUU9RRnQ3WG02OVl0RGh0T2FWbDgKL0o4dlVuSTBtWmxtT2pjTlRoYnVPZDlNSDlRdGxIRUMxMlhYdHJNb3Fsb0U2a05TT0pJalNxYm9wcDRXc1BqcApvZTZ0Nkdyd1RhOHBHeUJWWS90Mi85Ym5ORHVPVlpjODBaODdtY2gzcDNQclBqU3h5di9saGxYMFMwYUdHTkhTCk1XVjdUa25OaGo1TWlIRXFnZ1pZemtBWTkyd1JoVENnU1A2M0VNcitUWXFudXVuMXJHbndPYm95TDR2aFRpV0UKcU42UDNCTFlCZ1FpMllDTDludEJrOEl6RHZyd096dW5GVnhhZ0g5SVVoY0NnZ0VCQUwzQXlLa1BlOENWUmR6cQpzL284VkJDZmFSOFhhUGRnSGxTek1BSXZpNXEwNENqckRyMlV3MHZwTVdnM1hOZ0xUT3g5bFJpd3NrYk9SRmxHCmhhd3hRUWlBdkk0SE9WTlBTU0R1WHVNTG5USTQ0S0RFNlMrY2cxU0VMS2pWbDVqcDNFOEpkL1RJMVpLc0xBQUsKZTNHakM5UC9ZbE8xL21ndW4xNjVkWk01cFAwWHBPb2FaeFV2RHFFTktyekR0V1g0RngyOTZlUzdaSFJodFpCNwovQ2t1VUhlcmxrN2RDNnZzdWhTaTh2eTM3c0tPbmQ0K3c4cVM4czhZYVZxSDl3ZzVScUxxakp0bmJBUnc3alVDCm9KQ053M1hNdnc3clhaYzRTbnhVQUNMRGJNV2lLQy9xL1ZGWW9oTEs2WkpUVkJscWd5cjBSYzBRWmpDMlNJb0kKMjRwRWt3VUNnZ0VCQUpqb0FJVVNsVFY0WlVwaExXN3g4WkxPa01UWjBVdFFyd2NPR0hSYndPUUxGeUNGMVFWNQppejNiR2s4SmZyZHpVdk1sTmREZm9uQXVHTHhQa3VTVEUxWlg4L0xVRkJveXhyV3dvZ0cxaUtwME11QTV6em90CjROai9DbUtCQVkvWnh2anA5M2RFS21aZGxWQkdmeUFMeWpmTW5MWUovZXh5L09YSnhPUktZTUttSHg4M08zRWsKMWhvb0FwbTZabTIzMjRGME1iVU1ham5Idld2ZjhHZGJTNk5zcHd4L0dkbk1tYVMrdUJMVUhVMkNLbmc1bEIwVAp4OWJITmY0dXlPbTR0dXRmNzhCd1R5V3UreEdrVW0zZ2VZMnkvR1hqdDZyY2l1ajFGNzFDenZzcXFmZThTcDdJCnd6SHdxcTNzVHR5S2lCYTZuYUdEYWpNR1pKYSt4MVZJV204Q2dnRUJBT001ajFZR25Ba0pxR0czQWJSVDIvNUMKaVVxN0loYkswOGZsSGs5a2YwUlVjZWc0ZVlKY3dIRXJVaE4rdWQyLzE3MC81dDYra0JUdTVZOUg3bkpLREtESQpoeEg5SStyamNlVkR0RVNTRkluSXdDQ1lrOHhOUzZ0cHZMV1U5b0pibGFKMlZsalV2NGRFWGVQb0hkREh1Zk9ZClVLa0lsV2E3Uit1QzNEOHF5U1JrQnFLa3ZXZ1RxcFNmTVNkc1ZTeFIzU2Q4SVhFSHFjTDNUNEtMWGtYNEdEamYKMmZOSTFpZkx6ekhJMTN3Tk5IUTVRNU9SUC9pell2QzVzZkx4U2ZIUXJiMXJZVkpKWkI5ZjVBUjRmWFpHSVFsbApjMG8xd0JmZFlqMnZxVDlpR09IQnNSSTlSL2M2RzJQcUt3aFRpSzJVR2lmVFNEUVFuUkF6b2tpQVkrbE8vUjQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== | ||||||
							
								
								
									
										1
									
								
								auth/token/jwt/test/sample_key.pub
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								auth/token/jwt/test/sample_key.pub
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUE4U2JKUDVYYkVpZFJtNWIyc05wTApHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkL1JsOS8xcE1WN01pTzNMSHd0aEhDMkJSWXFyKzF3RmRvWkNHCkJZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUwQkgvbFhRTXF5RXFGNU1JMnplakM0ek16cjE1T04rZ0U0Sm4KaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtL21VZElUL0xhRjdrUXh5WUs1VktuK05nT1d6TWx6S0FBcENuNwpUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsbzlqRGpsWTVvQk9jemZxZU5XSEs1R1hCN1F3cExOaDk0NlB6ClpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDV3bEVxOGhOaG1obTVOTmUvTytHZ2pCRE5TZlVoMDYrcTRuZ20KYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1b1J0UWhnaVk5MTBwUGY5YmF1WFdxd1VDVWE0cXNIempLUjBMLwpOMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVcmdMclBiRUI5ZWdjR2szOCticEtzM1o2UnI1K3RuRDFCSVBJCkZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVVUR0R4VXg4b2o4VkllUm5XRHE2TWMxaUpwOFV5Y2lCSVRSdHcKNGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMGxhUXpBdVAzYWlXWElNcDJzYzhTYytCbCtMalhtQm94QnJhQgpIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YZ0ZLU3NJbkhEckhWT3lXUEJlM2ZhZEVjNzdiK25iL2V4T3pyCjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo= | ||||||
							
								
								
									
										78
									
								
								auth/token/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								auth/token/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | package token | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/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 | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								auth/token/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								auth/token/token.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | package token | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v2/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"` | ||||||
|  | } | ||||||
| @@ -28,6 +28,7 @@ type Event interface { | |||||||
| 	Topic() string | 	Topic() string | ||||||
| 	Message() *Message | 	Message() *Message | ||||||
| 	Ack() error | 	Ack() error | ||||||
|  | 	Error() error | ||||||
| } | } | ||||||
|  |  | ||||||
| // Subscriber is a convenience return type for the Subscribe method | // Subscriber is a convenience return type for the Subscribe method | ||||||
| @@ -38,13 +39,9 @@ type Subscriber interface { | |||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	DefaultBroker Broker = newHttpBroker() | 	DefaultBroker Broker = NewBroker() | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewBroker(opts ...Option) Broker { |  | ||||||
| 	return newHttpBroker(opts...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func Init(opts ...Option) error { | func Init(opts ...Option) error { | ||||||
| 	return DefaultBroker.Init(opts...) | 	return DefaultBroker.Init(opts...) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,47 +0,0 @@ | |||||||
| package broker |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/micro/go-micro/registry" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| 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", |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| ) |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user