Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
94bd1025a6 | ||
|
7be4a67673 | ||
|
3e6ac73cfe | ||
|
aef6878ee0 | ||
|
81aa8e0231 | ||
|
c28f625cd4 | ||
|
5b161b88f7 | ||
|
cca8826a1f |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: micro
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,9 +1,10 @@
|
||||
## Pull Request template
|
||||
Please, go through these steps before clicking submit on this PR.
|
||||
|
||||
1. Give a descriptive title to your PR.
|
||||
2. Provide a description of your changes.
|
||||
3. Make sure you have some relevant tests.
|
||||
4. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if applicable).
|
||||
1. Make sure this PR targets the `develop` branch. We follow the git-flow branching model.
|
||||
2. Give a descriptive title to your PR.
|
||||
3. Provide a description of your changes.
|
||||
4. Make sure you have some relevant tests.
|
||||
5. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if applicable).
|
||||
|
||||
**PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING**
|
||||
|
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -1,19 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
# Maintain dependencies for Golang
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
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
|
62
.github/workflows/build.yml
vendored
62
.github/workflows/build.yml
vendored
@@ -1,62 +0,0 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: setup
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
- name: cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
- name: sdk checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: sdk deps
|
||||
run: go get -v -t -d ./...
|
||||
- name: sdk test
|
||||
env:
|
||||
INTEGRATION_TESTS: yes
|
||||
run: go test -mod readonly -v ./...
|
||||
- name: tests checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: unistack-org/micro-tests
|
||||
ref: refs/heads/master
|
||||
path: micro-tests
|
||||
fetch-depth: 1
|
||||
- name: tests deps
|
||||
run: |
|
||||
cd micro-tests
|
||||
go mod edit -replace="github.com/unistack-org/micro/v3=../"
|
||||
go get -v -t -d ./...
|
||||
- name: tests test
|
||||
env:
|
||||
INTEGRATION_TESTS: yes
|
||||
run: cd micro-tests && go test -mod readonly -v ./...
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||
version: v1.39
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
# Optional: golangci-lint command line arguments.
|
||||
# args: --issues-exit-code=0
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
22
.github/workflows/docker.yml
vendored
Normal file
22
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
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 }}
|
||||
tag_names: true
|
34
.github/workflows/micro-examples.yml
vendored
Normal file
34
.github/workflows/micro-examples.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Build all github.com/micro/examples
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'release-**'
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build repos
|
||||
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 this code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: 'go-micro'
|
||||
|
||||
- name: Check out code examples
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'micro/examples'
|
||||
path: 'examples'
|
||||
|
||||
- name: Build all
|
||||
run: $GITHUB_WORKSPACE/go-micro/.github/workflows/scripts/build-all-examples.sh $GITHUB_SHA
|
||||
working-directory: examples
|
||||
|
34
.github/workflows/micro-main.yml
vendored
Normal file
34
.github/workflows/micro-main.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Build and test micro
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'release-**'
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build and test micro
|
||||
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 this code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: 'go-micro'
|
||||
|
||||
- name: Check out micro
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'micro/micro'
|
||||
path: 'micro'
|
||||
|
||||
- name: Build all
|
||||
run: $GITHUB_WORKSPACE/go-micro/.github/workflows/scripts/build-micro.sh $GITHUB_SHA
|
||||
working-directory: micro
|
||||
|
74
.github/workflows/pr.yml
vendored
74
.github/workflows/pr.yml
vendored
@@ -1,62 +1,28 @@
|
||||
name: prbuild
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
name: PR Sanity Check
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
|
||||
prtest:
|
||||
name: PR sanity check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: setup
|
||||
uses: actions/setup-go@v2
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
- name: cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
- name: sdk checkout
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- name: sdk deps
|
||||
run: go get -v -t -d ./...
|
||||
- name: sdk test
|
||||
env:
|
||||
INTEGRATION_TESTS: yes
|
||||
run: go test -mod readonly -v ./...
|
||||
- name: tests checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: unistack-org/micro-tests
|
||||
ref: refs/heads/master
|
||||
path: micro-tests
|
||||
fetch-depth: 1
|
||||
- name: tests deps
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
cd micro-tests
|
||||
go mod edit -replace="github.com/unistack-org/micro/v3=../"
|
||||
go get -v -t -d ./...
|
||||
- name: tests test
|
||||
|
||||
- name: Run tests
|
||||
id: tests
|
||||
env:
|
||||
INTEGRATION_TESTS: yes
|
||||
run: cd micro-tests && go test -mod readonly -v ./...
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||
version: v1.39
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
# Optional: golangci-lint command line arguments.
|
||||
# args: --issues-exit-code=0
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
IN_TRAVIS_CI: yes
|
||||
run: go test -v ./...
|
||||
|
41
.github/workflows/scripts/build-all-examples.sh
vendored
Executable file
41
.github/workflows/scripts/build-all-examples.sh
vendored
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# set -x
|
||||
|
||||
function build_binary {
|
||||
echo building $1
|
||||
pushd $1
|
||||
go build -o _main
|
||||
local ret=$?
|
||||
if [ $ret -gt 0 ]; then
|
||||
failed=1
|
||||
failed_arr+=($1)
|
||||
fi
|
||||
popd
|
||||
}
|
||||
|
||||
function is_main {
|
||||
grep "package main" -l -dskip $1/*.go > /dev/null 2>&1
|
||||
}
|
||||
|
||||
|
||||
function check_dir {
|
||||
is_main $1
|
||||
local ret=$?
|
||||
if [ $ret == 0 ]; then
|
||||
build_binary $1 $2
|
||||
fi
|
||||
for filename in $1/*; do
|
||||
if [ -d $filename ]; then
|
||||
check_dir $filename $2
|
||||
fi
|
||||
done
|
||||
}
|
||||
failed_arr=()
|
||||
failed=0
|
||||
go mod edit -replace github.com/micro/go-micro/v2=github.com/micro/go-micro/v2@$1
|
||||
check_dir . $1
|
||||
if [ $failed -gt 0 ]; then
|
||||
echo Some builds failed
|
||||
printf '%s\n' "${failed_arr[@]}"
|
||||
fi
|
||||
exit $failed
|
14
.github/workflows/scripts/build-micro.sh
vendored
Executable file
14
.github/workflows/scripts/build-micro.sh
vendored
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# set -x
|
||||
|
||||
failed=0
|
||||
go mod edit -replace github.com/micro/go-micro/v2=github.com/micro/go-micro/v2@$1
|
||||
# basic test, build the binary
|
||||
go build
|
||||
failed=$?
|
||||
if [ $failed -gt 0 ]; then
|
||||
exit $failed
|
||||
fi
|
||||
# unit tests
|
||||
IN_TRAVIS_CI=yes go test -v ./...
|
||||
# TODO integration tests
|
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 }}
|
||||
|
@@ -1,44 +1,26 @@
|
||||
run:
|
||||
concurrency: 4
|
||||
deadline: 5m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable:
|
||||
- fieldalignment
|
||||
|
||||
deadline: 10m
|
||||
linters:
|
||||
enable:
|
||||
- govet
|
||||
- deadcode
|
||||
- errcheck
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
- bodyclose
|
||||
- gci
|
||||
- goconst
|
||||
- gocritic
|
||||
- gosimple
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
- golint
|
||||
- gosec
|
||||
- makezero
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- noctx
|
||||
- prealloc
|
||||
- unconvert
|
||||
- unparam
|
||||
disable-all: false
|
||||
enable-all: false
|
||||
enable:
|
||||
- megacheck
|
||||
- staticcheck
|
||||
- deadcode
|
||||
- varcheck
|
||||
- gosimple
|
||||
- unused
|
||||
- prealloc
|
||||
- scopelint
|
||||
- gocritic
|
||||
- goimports
|
||||
- unconvert
|
||||
- govet
|
||||
- nakedret
|
||||
- structcheck
|
||||
- gosec
|
||||
disable:
|
||||
- maligned
|
||||
- interfacer
|
||||
- typecheck
|
||||
- dupl
|
||||
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM golang:1.13-alpine
|
||||
|
||||
RUN mkdir /user && \
|
||||
echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \
|
||||
echo 'nobody:x:65534:' > /user/group
|
||||
|
||||
ENV GO111MODULE=on
|
||||
RUN apk --no-cache add make git gcc libtool musl-dev ca-certificates dumb-init && \
|
||||
rm -rf /var/cache/apk/* /tmp/*
|
||||
|
||||
WORKDIR /
|
||||
COPY ./go.mod ./go.sum ./
|
||||
RUN go mod download && rm go.mod go.sum
|
3
LICENSE
3
LICENSE
@@ -176,8 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015-2020 Asim Aslam.
|
||||
Copyright 2019-2020 Unistack LLC.
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
47
README.md
47
README.md
@@ -1,14 +1,16 @@
|
||||
# Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/unistack-org/micro/v3?tab=overview) [](https://github.com/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Amaster+event%3Apush) [](https://goreportcard.com/report/github.com/unistack-org/micro) [](https://unistack-org.slack.com/messages/default)
|
||||
# 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)
|
||||
|
||||
Micro is a standard library for microservices.
|
||||
Go Micro is a framework for distributed systems development.
|
||||
|
||||
## Overview
|
||||
|
||||
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
|
||||
but everything can be easily swapped out.
|
||||
|
||||
## Features
|
||||
|
||||
Micro abstracts away the details of distributed systems. Here are the main features.
|
||||
Go Micro abstracts away the details of distributed systems. Here are the main features.
|
||||
|
||||
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
|
||||
zero trust networking by providing every service an identity and certificates. This additionally includes rule
|
||||
@@ -21,7 +23,8 @@ level config from any source such as env vars, file, etcd. You can merge the sou
|
||||
CockroachDB by default. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
||||
|
||||
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
||||
development. When service A needs to speak to service B it needs the location of that service.
|
||||
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
|
||||
multicast DNS (mdns), a zeroconf system.
|
||||
|
||||
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
|
||||
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
|
||||
@@ -29,24 +32,42 @@ across the services and retry a different node if there's a problem.
|
||||
|
||||
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
||||
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
||||
and server handle this by default.
|
||||
and server handle this by default. This includes protobuf and json by default.
|
||||
|
||||
- **Transport** - gRPC or http 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.
|
||||
- **gRPC Transport** - gRPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
|
||||
|
||||
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
||||
Event notifications are a core pattern in micro service development.
|
||||
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
||||
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
|
||||
|
||||
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
|
||||
leadership are built in as a Sync interface. When using an eventually consistent database or scheduling use the Sync interface.
|
||||
|
||||
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
|
||||
are pluggable and allows Micro to be runtime agnostic.
|
||||
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
|
||||
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology. Find plugins in
|
||||
[github.com/micro/go-plugins](https://github.com/micro/go-plugins).
|
||||
|
||||
## Getting Started
|
||||
|
||||
To be created.
|
||||
To make use of Go Micro
|
||||
|
||||
```golang
|
||||
import "github.com/micro/go-micro/v2"
|
||||
|
||||
// create a new service
|
||||
service := micro.NewService(
|
||||
micro.Name("helloworld"),
|
||||
)
|
||||
|
||||
// initialise flags
|
||||
service.Init()
|
||||
|
||||
// start the service
|
||||
service.Run()
|
||||
```
|
||||
|
||||
See the [docs](https://dev.m3o.com) for detailed information on the architecture, installation and use of go-micro.
|
||||
|
||||
## License
|
||||
|
||||
Micro is Apache 2.0 licensed.
|
||||
Go Micro is Apache 2.0 licensed.
|
||||
|
||||
|
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-architect
|
2
agent/agent.go
Normal file
2
agent/agent.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package agent provides an interface for building robots
|
||||
package agent
|
54
agent/command/command.go
Normal file
54
agent/command/command.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Package command is an interface for defining bot commands
|
||||
package command
|
||||
|
||||
var (
|
||||
// Commmands keyed by golang/regexp patterns
|
||||
// regexp.Match(key, input) is used to match
|
||||
Commands = map[string]Command{}
|
||||
)
|
||||
|
||||
// Command is the interface for specific named
|
||||
// commands executed via plugins or the bot.
|
||||
type Command interface {
|
||||
// Executes the command with args passed in
|
||||
Exec(args ...string) ([]byte, error)
|
||||
// Usage of the command
|
||||
Usage() string
|
||||
// Description of the command
|
||||
Description() string
|
||||
// Name of the command
|
||||
String() string
|
||||
}
|
||||
|
||||
type cmd struct {
|
||||
name string
|
||||
usage string
|
||||
description string
|
||||
exec func(args ...string) ([]byte, error)
|
||||
}
|
||||
|
||||
func (c *cmd) Description() string {
|
||||
return c.description
|
||||
}
|
||||
|
||||
func (c *cmd) Exec(args ...string) ([]byte, error) {
|
||||
return c.exec(args...)
|
||||
}
|
||||
|
||||
func (c *cmd) Usage() string {
|
||||
return c.usage
|
||||
}
|
||||
|
||||
func (c *cmd) String() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// NewCommand helps quickly create a new command
|
||||
func NewCommand(name, usage, description string, exec func(args ...string) ([]byte, error)) Command {
|
||||
return &cmd{
|
||||
name: name,
|
||||
usage: usage,
|
||||
description: description,
|
||||
exec: exec,
|
||||
}
|
||||
}
|
65
agent/command/command_test.go
Normal file
65
agent/command/command_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommand(t *testing.T) {
|
||||
c := &cmd{
|
||||
name: "test",
|
||||
usage: "test usage",
|
||||
description: "test description",
|
||||
exec: func(args ...string) ([]byte, error) {
|
||||
return []byte("test"), nil
|
||||
},
|
||||
}
|
||||
|
||||
if c.String() != c.name {
|
||||
t.Fatalf("expected name %s got %s", c.name, c.String())
|
||||
}
|
||||
|
||||
if c.Usage() != c.usage {
|
||||
t.Fatalf("expected usage %s got %s", c.usage, c.Usage())
|
||||
}
|
||||
|
||||
if c.Description() != c.description {
|
||||
t.Fatalf("expected description %s got %s", c.description, c.Description())
|
||||
}
|
||||
|
||||
if r, err := c.Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(r) != "test" {
|
||||
t.Fatalf("expected exec result test got %s", string(r))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
c := &cmd{
|
||||
name: "test",
|
||||
usage: "test usage",
|
||||
description: "test description",
|
||||
exec: func(args ...string) ([]byte, error) {
|
||||
return []byte("test"), nil
|
||||
},
|
||||
}
|
||||
|
||||
nc := NewCommand(c.name, c.usage, c.description, c.exec)
|
||||
|
||||
if nc.String() != c.name {
|
||||
t.Fatalf("expected name %s got %s", c.name, nc.String())
|
||||
}
|
||||
|
||||
if nc.Usage() != c.usage {
|
||||
t.Fatalf("expected usage %s got %s", c.usage, nc.Usage())
|
||||
}
|
||||
|
||||
if nc.Description() != c.description {
|
||||
t.Fatalf("expected description %s got %s", c.description, nc.Description())
|
||||
}
|
||||
|
||||
if r, err := nc.Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(r) != "test" {
|
||||
t.Fatalf("expected exec result test got %s", string(r))
|
||||
}
|
||||
}
|
22
agent/input/discord/README.md
Normal file
22
agent/input/discord/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Discord input for micro-bot
|
||||
[Discord](https://discordapp.com) support for micro bot based on [discordgo](github.com/bwmarrin/discordgo).
|
||||
|
||||
This was originally written by Aleksandr Tihomirov (@zet4) and can be found at https://github.com/zet4/micro-misc/.
|
||||
|
||||
## Options
|
||||
### discord_token
|
||||
|
||||
You have to supply an application token via `--discord_token`.
|
||||
|
||||
Head over to Discord's [developer introduction](https://discordapp.com/developers/docs/intro)
|
||||
to learn how to create applications and how the API works.
|
||||
|
||||
### discord_prefix
|
||||
|
||||
Set a command prefix with `--discord_prefix`. The default prefix is `Micro `.
|
||||
You can mention the bot or use the prefix to run a command.
|
||||
|
||||
### discord_whitelist
|
||||
|
||||
Pass a list of comma-separated user IDs with `--discord_whitelist`. Only allow
|
||||
these users to use the bot.
|
116
agent/input/discord/conn.go
Normal file
116
agent/input/discord/conn.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
type discordConn struct {
|
||||
master *discordInput
|
||||
exit chan struct{}
|
||||
recv chan *discordgo.Message
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func newConn(master *discordInput) *discordConn {
|
||||
conn := &discordConn{
|
||||
master: master,
|
||||
exit: make(chan struct{}),
|
||||
recv: make(chan *discordgo.Message),
|
||||
}
|
||||
|
||||
conn.master.session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
if m.Author.ID == master.botID {
|
||||
return
|
||||
}
|
||||
|
||||
whitelisted := false
|
||||
for _, ID := range conn.master.whitelist {
|
||||
if m.Author.ID == ID {
|
||||
whitelisted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(master.whitelist) > 0 && !whitelisted {
|
||||
return
|
||||
}
|
||||
|
||||
var valid bool
|
||||
m.Message.Content, valid = conn.master.prefixfn(m.Message.Content)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
conn.recv <- m.Message
|
||||
})
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func (dc *discordConn) Recv(event *input.Event) error {
|
||||
for {
|
||||
select {
|
||||
case <-dc.exit:
|
||||
return errors.New("connection closed")
|
||||
case msg := <-dc.recv:
|
||||
|
||||
event.From = msg.ChannelID + ":" + msg.Author.ID
|
||||
event.To = dc.master.botID
|
||||
event.Type = input.TextEvent
|
||||
event.Data = []byte(msg.Content)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ChunkString(s string, chunkSize int) []string {
|
||||
var chunks []string
|
||||
runes := []rune(s)
|
||||
|
||||
if len(runes) == 0 {
|
||||
return []string{s}
|
||||
}
|
||||
|
||||
for i := 0; i < len(runes); i += chunkSize {
|
||||
nn := i + chunkSize
|
||||
if nn > len(runes) {
|
||||
nn = len(runes)
|
||||
}
|
||||
chunks = append(chunks, string(runes[i:nn]))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func (dc *discordConn) Send(e *input.Event) error {
|
||||
fields := strings.Split(e.To, ":")
|
||||
for _, chunk := range ChunkString(string(e.Data), 2000) {
|
||||
_, err := dc.master.session.ChannelMessageSend(fields[0], chunk)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error("[bot][loop][send]", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *discordConn) Close() error {
|
||||
if err := dc.master.session.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-dc.exit:
|
||||
return nil
|
||||
default:
|
||||
close(dc.exit)
|
||||
}
|
||||
return nil
|
||||
}
|
153
agent/input/discord/discord.go
Normal file
153
agent/input/discord/discord.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/micro/cli/v2"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
)
|
||||
|
||||
func init() {
|
||||
input.Inputs["discord"] = newInput()
|
||||
}
|
||||
|
||||
func newInput() *discordInput {
|
||||
return &discordInput{}
|
||||
}
|
||||
|
||||
type discordInput struct {
|
||||
token string
|
||||
whitelist []string
|
||||
prefix string
|
||||
prefixfn func(string) (string, bool)
|
||||
botID string
|
||||
|
||||
session *discordgo.Session
|
||||
|
||||
sync.Mutex
|
||||
running bool
|
||||
exit chan struct{}
|
||||
}
|
||||
|
||||
func (d *discordInput) Flags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "discord_token",
|
||||
EnvVars: []string{"MICRO_DISCORD_TOKEN"},
|
||||
Usage: "Discord token (prefix with Bot if it's for bot account)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "discord_whitelist",
|
||||
EnvVars: []string{"MICRO_DISCORD_WHITELIST"},
|
||||
Usage: "Discord Whitelist (seperated by ,)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "discord_prefix",
|
||||
Usage: "Discord Prefix",
|
||||
EnvVars: []string{"MICRO_DISCORD_PREFIX"},
|
||||
Value: "Micro ",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *discordInput) Init(ctx *cli.Context) error {
|
||||
token := ctx.String("discord_token")
|
||||
whitelist := ctx.String("discord_whitelist")
|
||||
prefix := ctx.String("discord_prefix")
|
||||
|
||||
if len(token) == 0 {
|
||||
return errors.New("require token")
|
||||
}
|
||||
|
||||
d.token = token
|
||||
d.prefix = prefix
|
||||
|
||||
if len(whitelist) > 0 {
|
||||
d.whitelist = strings.Split(whitelist, ",")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *discordInput) Start() error {
|
||||
if len(d.token) == 0 {
|
||||
return errors.New("missing discord configuration")
|
||||
}
|
||||
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
if d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
d.session, err = discordgo.New("Bot " + d.token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := d.session.User("@me")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.botID = u.ID
|
||||
d.prefixfn = CheckPrefixFactory(fmt.Sprintf("<@%s> ", d.botID), fmt.Sprintf("<@!%s> ", d.botID), d.prefix)
|
||||
|
||||
d.exit = make(chan struct{})
|
||||
d.running = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *discordInput) Stream() (input.Conn, error) {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
if !d.running {
|
||||
return nil, errors.New("not running")
|
||||
}
|
||||
|
||||
//Fire-n-forget close just in case...
|
||||
d.session.Close()
|
||||
|
||||
conn := newConn(d)
|
||||
if err := d.session.Open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (d *discordInput) Stop() error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
if !d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
close(d.exit)
|
||||
d.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *discordInput) String() string {
|
||||
return "discord"
|
||||
}
|
||||
|
||||
// CheckPrefixFactory Creates a prefix checking function and stuff.
|
||||
func CheckPrefixFactory(prefixes ...string) func(string) (string, bool) {
|
||||
return func(content string) (string, bool) {
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(content, prefix) {
|
||||
return strings.TrimPrefix(content, prefix), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
55
agent/input/input.go
Normal file
55
agent/input/input.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Package input is an interface for bot inputs
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/micro/cli/v2"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
TextEvent EventType = "text"
|
||||
)
|
||||
|
||||
var (
|
||||
// Inputs keyed by name
|
||||
// Example slack or hipchat
|
||||
Inputs = map[string]Input{}
|
||||
)
|
||||
|
||||
// Event is the unit sent and received
|
||||
type Event struct {
|
||||
Type EventType
|
||||
From string
|
||||
To string
|
||||
Data []byte
|
||||
Meta map[string]interface{}
|
||||
}
|
||||
|
||||
// Input is an interface for sources which
|
||||
// provide a way to communicate with the bot.
|
||||
// Slack, HipChat, XMPP, etc.
|
||||
type Input interface {
|
||||
// Provide cli flags
|
||||
Flags() []cli.Flag
|
||||
// Initialise input using cli context
|
||||
Init(*cli.Context) error
|
||||
// Stream events from the input
|
||||
Stream() (Conn, error)
|
||||
// Start the input
|
||||
Start() error
|
||||
// Stop the input
|
||||
Stop() error
|
||||
// name of the input
|
||||
String() string
|
||||
}
|
||||
|
||||
// Conn interface provides a way to
|
||||
// send and receive events. Send and
|
||||
// Recv both block until succeeding
|
||||
// or failing.
|
||||
type Conn interface {
|
||||
Close() error
|
||||
Recv(*Event) error
|
||||
Send(*Event) error
|
||||
}
|
160
agent/input/slack/conn.go
Normal file
160
agent/input/slack/conn.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package slack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/nlopes/slack"
|
||||
)
|
||||
|
||||
// Satisfies the input.Conn interface
|
||||
type slackConn struct {
|
||||
auth *slack.AuthTestResponse
|
||||
rtm *slack.RTM
|
||||
exit chan bool
|
||||
|
||||
sync.Mutex
|
||||
names map[string]string
|
||||
}
|
||||
|
||||
func (s *slackConn) run() {
|
||||
// func retrieves user names and maps to IDs
|
||||
setNames := func() {
|
||||
names := make(map[string]string)
|
||||
users, err := s.rtm.Client.GetUsers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
names[user.ID] = user.Name
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
s.names = names
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
setNames()
|
||||
|
||||
t := time.NewTicker(time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.exit:
|
||||
return
|
||||
case <-t.C:
|
||||
setNames()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slackConn) getName(id string) string {
|
||||
s.Lock()
|
||||
name := s.names[id]
|
||||
s.Unlock()
|
||||
return name
|
||||
}
|
||||
|
||||
func (s *slackConn) Close() error {
|
||||
select {
|
||||
case <-s.exit:
|
||||
return nil
|
||||
default:
|
||||
close(s.exit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *slackConn) Recv(event *input.Event) error {
|
||||
if event == nil {
|
||||
return errors.New("event cannot be nil")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.exit:
|
||||
return errors.New("connection closed")
|
||||
case e := <-s.rtm.IncomingEvents:
|
||||
switch ev := e.Data.(type) {
|
||||
case *slack.MessageEvent:
|
||||
// only accept type message
|
||||
if ev.Type != "message" {
|
||||
continue
|
||||
}
|
||||
|
||||
// only accept DMs or messages to me
|
||||
switch {
|
||||
case strings.HasPrefix(ev.Channel, "D"):
|
||||
case strings.HasPrefix(ev.Text, s.auth.User):
|
||||
case strings.HasPrefix(ev.Text, fmt.Sprintf("<@%s>", s.auth.UserID)):
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip username from text
|
||||
switch {
|
||||
case strings.HasPrefix(ev.Text, s.auth.User):
|
||||
args := strings.Split(ev.Text, " ")[1:]
|
||||
ev.Text = strings.Join(args, " ")
|
||||
event.To = s.auth.User
|
||||
case strings.HasPrefix(ev.Text, fmt.Sprintf("<@%s>", s.auth.UserID)):
|
||||
args := strings.Split(ev.Text, " ")[1:]
|
||||
ev.Text = strings.Join(args, " ")
|
||||
event.To = s.auth.UserID
|
||||
}
|
||||
|
||||
if event.Meta == nil {
|
||||
event.Meta = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// fill in the blanks
|
||||
event.From = ev.Channel + ":" + ev.User
|
||||
event.Type = input.TextEvent
|
||||
event.Data = []byte(ev.Text)
|
||||
event.Meta["reply"] = ev
|
||||
return nil
|
||||
case *slack.InvalidAuthEvent:
|
||||
return errors.New("invalid credentials")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slackConn) Send(event *input.Event) error {
|
||||
var channel, message, name string
|
||||
|
||||
if len(event.To) == 0 {
|
||||
return errors.New("require Event.To")
|
||||
}
|
||||
|
||||
parts := strings.Split(event.To, ":")
|
||||
|
||||
if len(parts) == 2 {
|
||||
channel = parts[0]
|
||||
name = s.getName(parts[1])
|
||||
// try using reply meta
|
||||
} else if ev, ok := event.Meta["reply"]; ok {
|
||||
channel = ev.(*slack.MessageEvent).Channel
|
||||
name = s.getName(ev.(*slack.MessageEvent).User)
|
||||
}
|
||||
|
||||
// don't know where to send the message
|
||||
if len(channel) == 0 {
|
||||
return errors.New("could not determine who message is to")
|
||||
}
|
||||
|
||||
if len(name) == 0 || strings.HasPrefix(channel, "D") {
|
||||
message = string(event.Data)
|
||||
} else {
|
||||
message = fmt.Sprintf("@%s: %s", name, string(event.Data))
|
||||
}
|
||||
|
||||
s.rtm.SendMessage(s.rtm.NewOutgoingMessage(message, channel))
|
||||
return nil
|
||||
}
|
147
agent/input/slack/slack.go
Normal file
147
agent/input/slack/slack.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package slack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/cli/v2"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/nlopes/slack"
|
||||
)
|
||||
|
||||
type slackInput struct {
|
||||
debug bool
|
||||
token string
|
||||
|
||||
sync.Mutex
|
||||
running bool
|
||||
exit chan bool
|
||||
|
||||
api *slack.Client
|
||||
}
|
||||
|
||||
func init() {
|
||||
input.Inputs["slack"] = NewInput()
|
||||
}
|
||||
|
||||
func (p *slackInput) Flags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "slack_debug",
|
||||
Usage: "Slack debug output",
|
||||
EnvVars: []string{"MICRO_SLACK_DEBUG"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "slack_token",
|
||||
Usage: "Slack token",
|
||||
EnvVars: []string{"MICRO_SLACK_TOKEN"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *slackInput) Init(ctx *cli.Context) error {
|
||||
debug := ctx.Bool("slack_debug")
|
||||
token := ctx.String("slack_token")
|
||||
|
||||
if len(token) == 0 {
|
||||
return errors.New("missing slack token")
|
||||
}
|
||||
|
||||
p.debug = debug
|
||||
p.token = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *slackInput) Stream() (input.Conn, error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if !p.running {
|
||||
return nil, errors.New("not running")
|
||||
}
|
||||
|
||||
// test auth
|
||||
auth, err := p.api.AuthTest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtm := p.api.NewRTM()
|
||||
exit := make(chan bool)
|
||||
|
||||
go rtm.ManageConnection()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-p.exit:
|
||||
select {
|
||||
case <-exit:
|
||||
return
|
||||
default:
|
||||
close(exit)
|
||||
}
|
||||
case <-exit:
|
||||
}
|
||||
|
||||
rtm.Disconnect()
|
||||
}()
|
||||
|
||||
conn := &slackConn{
|
||||
auth: auth,
|
||||
rtm: rtm,
|
||||
exit: exit,
|
||||
names: make(map[string]string),
|
||||
}
|
||||
|
||||
go conn.run()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (p *slackInput) Start() error {
|
||||
if len(p.token) == 0 {
|
||||
return errors.New("missing slack token")
|
||||
}
|
||||
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if p.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
api := slack.New(p.token, slack.OptionDebug(p.debug))
|
||||
|
||||
// test auth
|
||||
_, err := api.AuthTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.api = api
|
||||
p.exit = make(chan bool)
|
||||
p.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *slackInput) Stop() error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if !p.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
close(p.exit)
|
||||
p.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *slackInput) String() string {
|
||||
return "slack"
|
||||
}
|
||||
|
||||
func NewInput() input.Input {
|
||||
return &slackInput{}
|
||||
}
|
18
agent/input/telegram/README.md
Normal file
18
agent/input/telegram/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Telegram Messenger input for micro bot
|
||||
[Telegram](https://telegram.org) support for micro bot based on [telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api).
|
||||
|
||||
## Options
|
||||
### --telegram_token (required)
|
||||
|
||||
Sets bot's token for interacting with API.
|
||||
|
||||
Head over to Telegram's [API documentation](https://core.telegram.org/bots/api)
|
||||
to learn how to create bots and how the API works.
|
||||
|
||||
### --telegram_debug
|
||||
|
||||
Sets the debug flag to make the bot's output verbose.
|
||||
|
||||
### --telegram_whitelist
|
||||
|
||||
Sets a list of comma-separated nicknames (without @ symbol in the beginning) for interacting with bot. Only these users can use the bot.
|
115
agent/input/telegram/conn.go
Normal file
115
agent/input/telegram/conn.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/forestgiant/sliceutil"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
tgbotapi "gopkg.in/telegram-bot-api.v4"
|
||||
)
|
||||
|
||||
type telegramConn struct {
|
||||
input *telegramInput
|
||||
|
||||
recv <-chan tgbotapi.Update
|
||||
exit chan bool
|
||||
|
||||
syncCond *sync.Cond
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func newConn(input *telegramInput) (*telegramConn, error) {
|
||||
conn := &telegramConn{
|
||||
input: input,
|
||||
}
|
||||
|
||||
conn.syncCond = sync.NewCond(&conn.mutex)
|
||||
|
||||
go conn.run()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (tc *telegramConn) run() {
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
updates, err := tc.input.api.GetUpdatesChan(u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tc.recv = updates
|
||||
tc.syncCond.Signal()
|
||||
|
||||
select {
|
||||
case <-tc.exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *telegramConn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *telegramConn) Recv(event *input.Event) error {
|
||||
if event == nil {
|
||||
return errors.New("event cannot be nil")
|
||||
}
|
||||
|
||||
for {
|
||||
if tc.recv == nil {
|
||||
tc.mutex.Lock()
|
||||
tc.syncCond.Wait()
|
||||
}
|
||||
|
||||
update := <-tc.recv
|
||||
|
||||
if update.Message == nil || (len(tc.input.whitelist) > 0 && !sliceutil.Contains(tc.input.whitelist, update.Message.From.UserName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.Meta == nil {
|
||||
event.Meta = make(map[string]interface{})
|
||||
}
|
||||
|
||||
event.Type = input.TextEvent
|
||||
event.From = update.Message.From.UserName
|
||||
event.To = tc.input.api.Self.UserName
|
||||
event.Data = []byte(update.Message.Text)
|
||||
event.Meta["chatId"] = update.Message.Chat.ID
|
||||
event.Meta["chatType"] = update.Message.Chat.Type
|
||||
event.Meta["messageId"] = update.Message.MessageID
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *telegramConn) Send(event *input.Event) error {
|
||||
messageText := strings.TrimSpace(string(event.Data))
|
||||
|
||||
chatId := event.Meta["chatId"].(int64)
|
||||
chatType := ChatType(event.Meta["chatType"].(string))
|
||||
|
||||
msgConfig := tgbotapi.NewMessage(chatId, messageText)
|
||||
msgConfig.ParseMode = tgbotapi.ModeHTML
|
||||
|
||||
if sliceutil.Contains([]ChatType{Group, Supergroup}, chatType) {
|
||||
msgConfig.ReplyToMessageID = event.Meta["messageId"].(int)
|
||||
}
|
||||
|
||||
_, err := tc.input.api.Send(msgConfig)
|
||||
|
||||
if err != nil {
|
||||
// probably it could be because of nested HTML tags -- telegram doesn't allow nested tags
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error("[telegram][Send] error:", err)
|
||||
}
|
||||
msgConfig.Text = "This bot couldn't send the response (Internal error)"
|
||||
tc.input.api.Send(msgConfig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
101
agent/input/telegram/telegram.go
Normal file
101
agent/input/telegram/telegram.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/cli/v2"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
tgbotapi "gopkg.in/telegram-bot-api.v4"
|
||||
)
|
||||
|
||||
type telegramInput struct {
|
||||
sync.Mutex
|
||||
|
||||
debug bool
|
||||
token string
|
||||
whitelist []string
|
||||
|
||||
api *tgbotapi.BotAPI
|
||||
}
|
||||
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
Private ChatType = "private"
|
||||
Group ChatType = "group"
|
||||
Supergroup ChatType = "supergroup"
|
||||
)
|
||||
|
||||
func init() {
|
||||
input.Inputs["telegram"] = &telegramInput{}
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Flags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "telegram_debug",
|
||||
EnvVars: []string{"MICRO_TELEGRAM_DEBUG"},
|
||||
Usage: "Telegram debug output",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "telegram_token",
|
||||
EnvVars: []string{"MICRO_TELEGRAM_TOKEN"},
|
||||
Usage: "Telegram token",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "telegram_whitelist",
|
||||
EnvVars: []string{"MICRO_TELEGRAM_WHITELIST"},
|
||||
Usage: "Telegram bot's users (comma-separated values)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Init(ctx *cli.Context) error {
|
||||
ti.debug = ctx.Bool("telegram_debug")
|
||||
ti.token = ctx.String("telegram_token")
|
||||
|
||||
whitelist := ctx.String("telegram_whitelist")
|
||||
|
||||
if whitelist != "" {
|
||||
ti.whitelist = strings.Split(whitelist, ",")
|
||||
}
|
||||
|
||||
if len(ti.token) == 0 {
|
||||
return errors.New("missing telegram token")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Stream() (input.Conn, error) {
|
||||
ti.Lock()
|
||||
defer ti.Unlock()
|
||||
|
||||
return newConn(ti)
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Start() error {
|
||||
ti.Lock()
|
||||
defer ti.Unlock()
|
||||
|
||||
api, err := tgbotapi.NewBotAPI(ti.token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ti.api = api
|
||||
|
||||
api.Debug = ti.debug
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *telegramInput) String() string {
|
||||
return "telegram"
|
||||
}
|
333
agent/proto/bot.pb.go
Normal file
333
agent/proto/bot.pb.go
Normal file
@@ -0,0 +1,333 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: agent/proto/bot.proto
|
||||
|
||||
package go_micro_bot
|
||||
|
||||
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 HelpRequest struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *HelpRequest) Reset() { *m = HelpRequest{} }
|
||||
func (m *HelpRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*HelpRequest) ProtoMessage() {}
|
||||
func (*HelpRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{0}
|
||||
}
|
||||
|
||||
func (m *HelpRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HelpRequest.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HelpRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HelpRequest.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *HelpRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HelpRequest.Merge(m, src)
|
||||
}
|
||||
func (m *HelpRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_HelpRequest.Size(m)
|
||||
}
|
||||
func (m *HelpRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HelpRequest.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HelpRequest proto.InternalMessageInfo
|
||||
|
||||
type HelpResponse struct {
|
||||
Usage string `protobuf:"bytes,1,opt,name=usage,proto3" json:"usage,omitempty"`
|
||||
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *HelpResponse) Reset() { *m = HelpResponse{} }
|
||||
func (m *HelpResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*HelpResponse) ProtoMessage() {}
|
||||
func (*HelpResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{1}
|
||||
}
|
||||
|
||||
func (m *HelpResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HelpResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HelpResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HelpResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *HelpResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HelpResponse.Merge(m, src)
|
||||
}
|
||||
func (m *HelpResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_HelpResponse.Size(m)
|
||||
}
|
||||
func (m *HelpResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HelpResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HelpResponse proto.InternalMessageInfo
|
||||
|
||||
func (m *HelpResponse) GetUsage() string {
|
||||
if m != nil {
|
||||
return m.Usage
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *HelpResponse) GetDescription() string {
|
||||
if m != nil {
|
||||
return m.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ExecRequest struct {
|
||||
Args []string `protobuf:"bytes,1,rep,name=args,proto3" json:"args,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ExecRequest) Reset() { *m = ExecRequest{} }
|
||||
func (m *ExecRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecRequest) ProtoMessage() {}
|
||||
func (*ExecRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{2}
|
||||
}
|
||||
|
||||
func (m *ExecRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ExecRequest.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ExecRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ExecRequest.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *ExecRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ExecRequest.Merge(m, src)
|
||||
}
|
||||
func (m *ExecRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_ExecRequest.Size(m)
|
||||
}
|
||||
func (m *ExecRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ExecRequest.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ExecRequest proto.InternalMessageInfo
|
||||
|
||||
func (m *ExecRequest) GetArgs() []string {
|
||||
if m != nil {
|
||||
return m.Args
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExecResponse struct {
|
||||
Result []byte `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ExecResponse) Reset() { *m = ExecResponse{} }
|
||||
func (m *ExecResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecResponse) ProtoMessage() {}
|
||||
func (*ExecResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{3}
|
||||
}
|
||||
|
||||
func (m *ExecResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ExecResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ExecResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ExecResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *ExecResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ExecResponse.Merge(m, src)
|
||||
}
|
||||
func (m *ExecResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_ExecResponse.Size(m)
|
||||
}
|
||||
func (m *ExecResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ExecResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ExecResponse proto.InternalMessageInfo
|
||||
|
||||
func (m *ExecResponse) GetResult() []byte {
|
||||
if m != nil {
|
||||
return m.Result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExecResponse) GetError() string {
|
||||
if m != nil {
|
||||
return m.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*HelpRequest)(nil), "go.micro.bot.HelpRequest")
|
||||
proto.RegisterType((*HelpResponse)(nil), "go.micro.bot.HelpResponse")
|
||||
proto.RegisterType((*ExecRequest)(nil), "go.micro.bot.ExecRequest")
|
||||
proto.RegisterType((*ExecResponse)(nil), "go.micro.bot.ExecResponse")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("agent/proto/bot.proto", fileDescriptor_79b974b8c77805fa) }
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// CommandClient is the client API for Command service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||
type CommandClient interface {
|
||||
Help(ctx context.Context, in *HelpRequest, opts ...grpc.CallOption) (*HelpResponse, error)
|
||||
Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error)
|
||||
}
|
||||
|
||||
type commandClient struct {
|
||||
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",
|
||||
}
|
110
agent/proto/bot.pb.micro.go
Normal file
110
agent/proto/bot.pb.micro.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: agent/proto/bot.proto
|
||||
|
||||
package go_micro_bot
|
||||
|
||||
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 Command service
|
||||
|
||||
func NewCommandEndpoints() []*api.Endpoint {
|
||||
return []*api.Endpoint{}
|
||||
}
|
||||
|
||||
// Client API for Command service
|
||||
|
||||
type CommandService interface {
|
||||
Help(ctx context.Context, in *HelpRequest, opts ...client.CallOption) (*HelpResponse, error)
|
||||
Exec(ctx context.Context, in *ExecRequest, opts ...client.CallOption) (*ExecResponse, error)
|
||||
}
|
||||
|
||||
type commandService struct {
|
||||
c client.Client
|
||||
name string
|
||||
}
|
||||
|
||||
func NewCommandService(name string, c client.Client) CommandService {
|
||||
return &commandService{
|
||||
c: c,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commandService) Help(ctx context.Context, in *HelpRequest, opts ...client.CallOption) (*HelpResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Command.Help", in)
|
||||
out := new(HelpResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *commandService) Exec(ctx context.Context, in *ExecRequest, opts ...client.CallOption) (*ExecResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Command.Exec", in)
|
||||
out := new(ExecResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for Command service
|
||||
|
||||
type CommandHandler interface {
|
||||
Help(context.Context, *HelpRequest, *HelpResponse) error
|
||||
Exec(context.Context, *ExecRequest, *ExecResponse) error
|
||||
}
|
||||
|
||||
func RegisterCommandHandler(s server.Server, hdlr CommandHandler, opts ...server.HandlerOption) error {
|
||||
type command interface {
|
||||
Help(ctx context.Context, in *HelpRequest, out *HelpResponse) error
|
||||
Exec(ctx context.Context, in *ExecRequest, out *ExecResponse) error
|
||||
}
|
||||
type Command struct {
|
||||
command
|
||||
}
|
||||
h := &commandHandler{hdlr}
|
||||
return s.Handle(s.NewHandler(&Command{h}, opts...))
|
||||
}
|
||||
|
||||
type commandHandler struct {
|
||||
CommandHandler
|
||||
}
|
||||
|
||||
func (h *commandHandler) Help(ctx context.Context, in *HelpRequest, out *HelpResponse) error {
|
||||
return h.CommandHandler.Help(ctx, in, out)
|
||||
}
|
||||
|
||||
func (h *commandHandler) Exec(ctx context.Context, in *ExecRequest, out *ExecResponse) error {
|
||||
return h.CommandHandler.Exec(ctx, in, out)
|
||||
}
|
25
agent/proto/bot.proto
Normal file
25
agent/proto/bot.proto
Normal file
@@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package go.micro.bot;
|
||||
|
||||
service Command {
|
||||
rpc Help(HelpRequest) returns (HelpResponse) {};
|
||||
rpc Exec(ExecRequest) returns (ExecResponse) {};
|
||||
}
|
||||
|
||||
message HelpRequest {
|
||||
}
|
||||
|
||||
message HelpResponse {
|
||||
string usage = 1;
|
||||
string description = 2;
|
||||
}
|
||||
|
||||
message ExecRequest {
|
||||
repeated string args = 1;
|
||||
}
|
||||
|
||||
message ExecResponse {
|
||||
bytes result = 1;
|
||||
string error = 2;
|
||||
}
|
74
api/api.go
74
api/api.go
@@ -5,12 +5,10 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/unistack-org/micro/v3/metadata"
|
||||
"github.com/unistack-org/micro/v3/register"
|
||||
"github.com/unistack-org/micro/v3/server"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/micro/go-micro/v2/server"
|
||||
)
|
||||
|
||||
// Api interface
|
||||
type Api interface {
|
||||
// Initialise options
|
||||
Init(...Option) error
|
||||
@@ -20,34 +18,32 @@ type Api interface {
|
||||
Register(*Endpoint) error
|
||||
// Register a route
|
||||
Deregister(*Endpoint) error
|
||||
// Implementation of api
|
||||
// Implemenation of api
|
||||
String() string
|
||||
}
|
||||
|
||||
// Options holds the options
|
||||
type Options struct{}
|
||||
|
||||
// Option func signature
|
||||
type Option func(*Options) error
|
||||
|
||||
// Endpoint is a mapping between an RPC method and HTTP endpoint
|
||||
type Endpoint struct {
|
||||
// Name Greeter.Hello
|
||||
// RPC Method e.g. Greeter.Hello
|
||||
Name string
|
||||
// Desciption for endpoint
|
||||
// Description e.g what's this endpoint for
|
||||
Description string
|
||||
// Handler e.g rpc, proxy
|
||||
// API Handler e.g rpc, proxy
|
||||
Handler string
|
||||
// HTTP Host e.g example.com
|
||||
Host []string
|
||||
// HTTP Methods e.g GET, POST
|
||||
Method []string
|
||||
// HTTP Path e.g /greeter. Expect POSIX regex
|
||||
Path []string
|
||||
// Body destination
|
||||
// "*" or "" - top level message value
|
||||
// "string" - inner message value
|
||||
Body string
|
||||
// Host e.g example.com
|
||||
Host []string
|
||||
// Method e.g GET, POST
|
||||
Method []string
|
||||
// Path e.g /greeter. Expect POSIX regex
|
||||
Path []string
|
||||
// Stream flag
|
||||
Stream bool
|
||||
}
|
||||
@@ -56,10 +52,26 @@ type Endpoint struct {
|
||||
type Service struct {
|
||||
// Name of service
|
||||
Name string
|
||||
// Endpoint for this service
|
||||
// The endpoint for this service
|
||||
Endpoint *Endpoint
|
||||
// Services that provides service
|
||||
Services []*register.Service
|
||||
// Versions of this service
|
||||
Services []*registry.Service
|
||||
}
|
||||
|
||||
func strip(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func slice(s string) []string {
|
||||
var sl []string
|
||||
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
if str := strip(p); len(str) > 0 {
|
||||
sl = append(sl, strip(p))
|
||||
}
|
||||
}
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
// Encode encodes an endpoint to endpoint metadata
|
||||
@@ -85,30 +97,24 @@ func Encode(e *Endpoint) map[string]string {
|
||||
set("method", strings.Join(e.Method, ","))
|
||||
set("path", strings.Join(e.Path, ","))
|
||||
set("host", strings.Join(e.Host, ","))
|
||||
set("body", e.Body)
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
// Decode decodes endpoint metadata into an endpoint
|
||||
func Decode(e metadata.Metadata) *Endpoint {
|
||||
func Decode(e map[string]string) *Endpoint {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ep := &Endpoint{}
|
||||
ep.Name, _ = e.Get("endpoint")
|
||||
ep.Description, _ = e.Get("description")
|
||||
epmethod, _ := e.Get("method")
|
||||
ep.Method = []string{epmethod}
|
||||
eppath, _ := e.Get("path")
|
||||
ep.Path = []string{eppath}
|
||||
ephost, _ := e.Get("host")
|
||||
ep.Host = []string{ephost}
|
||||
ep.Handler, _ = e.Get("handler")
|
||||
ep.Body, _ = e.Get("body")
|
||||
|
||||
return ep
|
||||
return &Endpoint{
|
||||
Name: e["endpoint"],
|
||||
Description: e["description"],
|
||||
Method: slice(e["method"]),
|
||||
Path: slice(e["path"]),
|
||||
Host: slice(e["host"]),
|
||||
Handler: e["handler"],
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates an endpoint to guarantee it won't blow up when being served
|
||||
|
@@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
//nolint:gocyclo
|
||||
func TestEncoding(t *testing.T) {
|
||||
testData := []*Endpoint{
|
||||
nil,
|
||||
@@ -149,4 +148,5 @@ func TestValidate(t *testing.T) {
|
||||
if err := Validate(epPcreInvalid); err == nil {
|
||||
t.Fatalf("invalid pcre %v", epPcreInvalid.Path[0])
|
||||
}
|
||||
|
||||
}
|
||||
|
123
api/handler/api/api.go
Normal file
123
api/handler/api/api.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Package api provides an http-rpc handler which provides the entire http request over rpc
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
goapi "github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
api "github.com/micro/go-micro/v2/api/proto"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
"github.com/micro/go-micro/v2/errors"
|
||||
"github.com/micro/go-micro/v2/util/ctx"
|
||||
)
|
||||
|
||||
type apiHandler struct {
|
||||
opts handler.Options
|
||||
s *goapi.Service
|
||||
}
|
||||
|
||||
const (
|
||||
Handler = "api"
|
||||
)
|
||||
|
||||
// API handler is the default handler which takes api.Request and returns api.Response
|
||||
func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if a.opts.MaxRecvSize > 0 {
|
||||
bsize = a.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
request, err := requestToProto(r)
|
||||
if err != nil {
|
||||
er := errors.InternalServerError("go.micro.api", err.Error())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(er.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var service *goapi.Service
|
||||
|
||||
if a.s != nil {
|
||||
// we were given the service
|
||||
service = a.s
|
||||
} else if a.opts.Router != nil {
|
||||
// try get service from router
|
||||
s, err := a.opts.Router.Route(r)
|
||||
if err != nil {
|
||||
er := errors.InternalServerError("go.micro.api", err.Error())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(er.Error()))
|
||||
return
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
er := errors.InternalServerError("go.micro.api", "no route found")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(er.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// create request and response
|
||||
c := a.opts.Client
|
||||
req := c.NewRequest(service.Name, service.Endpoint.Name, request)
|
||||
rsp := &api.Response{}
|
||||
|
||||
// create the context from headers
|
||||
cx := ctx.FromRequest(r)
|
||||
// create strategy
|
||||
so := selector.WithStrategy(strategy(service.Services))
|
||||
|
||||
if err := c.Call(cx, req, rsp, client.WithSelectOption(so)); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
ce := errors.Parse(err.Error())
|
||||
switch ce.Code {
|
||||
case 0:
|
||||
w.WriteHeader(500)
|
||||
default:
|
||||
w.WriteHeader(int(ce.Code))
|
||||
}
|
||||
w.Write([]byte(ce.Error()))
|
||||
return
|
||||
} else if rsp.StatusCode == 0 {
|
||||
rsp.StatusCode = http.StatusOK
|
||||
}
|
||||
|
||||
for _, header := range rsp.GetHeader() {
|
||||
for _, val := range header.Values {
|
||||
w.Header().Add(header.Key, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(w.Header().Get("Content-Type")) == 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
w.WriteHeader(int(rsp.StatusCode))
|
||||
w.Write([]byte(rsp.Body))
|
||||
}
|
||||
|
||||
func (a *apiHandler) String() string {
|
||||
return "api"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &apiHandler{
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func WithService(s *goapi.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &apiHandler{
|
||||
opts: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
119
api/handler/api/util.go
Normal file
119
api/handler/api/util.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
api "github.com/micro/go-micro/v2/api/proto"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
"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) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing form: %v", err)
|
||||
}
|
||||
|
||||
req := &api.Request{
|
||||
Path: r.URL.Path,
|
||||
Method: r.Method,
|
||||
Header: make(map[string]*api.Pair),
|
||||
Get: make(map[string]*api.Pair),
|
||||
Post: make(map[string]*api.Pair),
|
||||
Url: r.URL.String(),
|
||||
}
|
||||
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
ct = "text/plain; charset=UTF-8" //default CT is text/plain
|
||||
r.Header.Set("Content-Type", ct)
|
||||
}
|
||||
|
||||
//set the body:
|
||||
if r.Body != nil {
|
||||
switch ct {
|
||||
case "application/x-www-form-urlencoded":
|
||||
// expect form vals in Post data
|
||||
default:
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err = buf.ReadFrom(r.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Set X-Forwarded-For if it does not exist
|
||||
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if prior, ok := r.Header["X-Forwarded-For"]; ok {
|
||||
ip = strings.Join(prior, ", ") + ", " + ip
|
||||
}
|
||||
|
||||
// Set the header
|
||||
req.Header["X-Forwarded-For"] = &api.Pair{
|
||||
Key: "X-Forwarded-For",
|
||||
Values: []string{ip},
|
||||
}
|
||||
}
|
||||
|
||||
// Host is stripped from net/http Headers so let's add it
|
||||
req.Header["Host"] = &api.Pair{
|
||||
Key: "Host",
|
||||
Values: []string{r.Host},
|
||||
}
|
||||
|
||||
// Get data
|
||||
for key, vals := range r.URL.Query() {
|
||||
header, ok := req.Get[key]
|
||||
if !ok {
|
||||
header = &api.Pair{
|
||||
Key: key,
|
||||
}
|
||||
req.Get[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
// Post data
|
||||
for key, vals := range r.PostForm {
|
||||
header, ok := req.Post[key]
|
||||
if !ok {
|
||||
header = &api.Pair{
|
||||
Key: key,
|
||||
}
|
||||
req.Post[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
for key, vals := range r.Header {
|
||||
header, ok := req.Header[key]
|
||||
if !ok {
|
||||
header = &api.Pair{
|
||||
Key: key,
|
||||
}
|
||||
req.Header[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// strategy is a hack for selection
|
||||
func strategy(services []*registry.Service) selector.Strategy {
|
||||
return func(_ []*registry.Service) selector.Next {
|
||||
// ignore input to this function, use services above
|
||||
return selector.Random(services)
|
||||
}
|
||||
}
|
46
api/handler/api/util_test.go
Normal file
46
api/handler/api/util_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestToProto(t *testing.T) {
|
||||
testData := []*http.Request{
|
||||
{
|
||||
Method: "GET",
|
||||
Header: http.Header{
|
||||
"Header": []string{"test"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "localhost",
|
||||
Path: "/foo/bar",
|
||||
RawQuery: "param1=value1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
p, err := requestToProto(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Path != d.URL.Path {
|
||||
t.Fatalf("Expected path %s got %s", d.URL.Path, p.Path)
|
||||
}
|
||||
if p.Method != d.Method {
|
||||
t.Fatalf("Expected method %s got %s", d.Method, p.Method)
|
||||
}
|
||||
for k, v := range d.Header {
|
||||
if val, ok := p.Header[k]; !ok {
|
||||
t.Fatalf("Expected header %s", k)
|
||||
} else {
|
||||
if val.Values[0] != v[0] {
|
||||
t.Fatalf("Expected val %s, got %s", val.Values[0], v[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
141
api/handler/event/event.go
Normal file
141
api/handler/event/event.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Package event provides a handler which publishes an event
|
||||
package event
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
proto "github.com/micro/go-micro/v2/api/proto"
|
||||
"github.com/micro/go-micro/v2/util/ctx"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
type event struct {
|
||||
opts handler.Options
|
||||
}
|
||||
|
||||
var (
|
||||
Handler = "event"
|
||||
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
|
||||
)
|
||||
|
||||
func eventName(parts []string) string {
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
func evRoute(ns, p string) (string, string) {
|
||||
p = path.Clean(p)
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
|
||||
if len(p) == 0 {
|
||||
return ns, "event"
|
||||
}
|
||||
|
||||
parts := strings.Split(p, "/")
|
||||
|
||||
// no path
|
||||
if len(parts) == 0 {
|
||||
// topic: namespace
|
||||
// action: event
|
||||
return strings.Trim(ns, "."), "event"
|
||||
}
|
||||
|
||||
// Treat /v[0-9]+ as versioning
|
||||
// /v1/foo/bar => topic: v1.foo action: bar
|
||||
if len(parts) >= 2 && versionRe.Match([]byte(parts[0])) {
|
||||
topic := ns + "." + strings.Join(parts[:2], ".")
|
||||
action := eventName(parts[1:])
|
||||
return topic, action
|
||||
}
|
||||
|
||||
// /foo => topic: ns.foo action: foo
|
||||
// /foo/bar => topic: ns.foo action: bar
|
||||
topic := ns + "." + strings.Join(parts[:1], ".")
|
||||
action := eventName(parts[1:])
|
||||
|
||||
return topic, action
|
||||
}
|
||||
|
||||
func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if e.opts.MaxRecvSize > 0 {
|
||||
bsize = e.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
|
||||
// request to topic:event
|
||||
// create event
|
||||
// publish to topic
|
||||
|
||||
topic, action := evRoute(e.opts.Namespace, r.URL.Path)
|
||||
|
||||
// create event
|
||||
ev := &proto.Event{
|
||||
Name: action,
|
||||
// TODO: dedupe event
|
||||
Id: fmt.Sprintf("%s-%s-%s", topic, action, uuid.New().String()),
|
||||
Header: make(map[string]*proto.Pair),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// set headers
|
||||
for key, vals := range r.Header {
|
||||
header, ok := ev.Header[key]
|
||||
if !ok {
|
||||
header = &proto.Pair{
|
||||
Key: key,
|
||||
}
|
||||
ev.Header[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
// set body
|
||||
if r.Method == "GET" {
|
||||
bytes, _ := json.Marshal(r.URL.Query())
|
||||
ev.Data = string(bytes)
|
||||
} else {
|
||||
// Read body
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
ev.Data = buf.String()
|
||||
}
|
||||
|
||||
// get client
|
||||
c := e.opts.Client
|
||||
|
||||
// create publication
|
||||
p := c.NewMessage(topic, ev)
|
||||
|
||||
// publish event
|
||||
if err := c.Publish(ctx.FromRequest(r), p); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *event) String() string {
|
||||
return "event"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
return &event{
|
||||
opts: handler.NewOptions(opts...),
|
||||
}
|
||||
}
|
100
api/handler/http/http.go
Normal file
100
api/handler/http/http.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Package http is a http reverse proxy handler
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "http"
|
||||
)
|
||||
|
||||
type httpHandler struct {
|
||||
options handler.Options
|
||||
|
||||
// set with different initialiser
|
||||
s *api.Service
|
||||
}
|
||||
|
||||
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := h.getService(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
if len(service) == 0 {
|
||||
w.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
|
||||
rp, err := url.Parse(service)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getService returns the service for this request from the selector
|
||||
func (h *httpHandler) getService(r *http.Request) (string, error) {
|
||||
var service *api.Service
|
||||
|
||||
if h.s != nil {
|
||||
// we were given the service
|
||||
service = h.s
|
||||
} else if h.options.Router != nil {
|
||||
// try get service from router
|
||||
s, err := h.options.Router.Route(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
return "", errors.New("no route found")
|
||||
}
|
||||
|
||||
// create a random selector
|
||||
next := selector.Random(service.Services)
|
||||
|
||||
// get the next node
|
||||
s, err := next()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s", s.Address), nil
|
||||
}
|
||||
|
||||
func (h *httpHandler) String() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
// NewHandler returns a http proxy handler
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return &httpHandler{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// WithService creates a handler with a service
|
||||
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return &httpHandler{
|
||||
options: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
127
api/handler/http/http_test.go
Normal file
127
api/handler/http/http_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/resolver/vpath"
|
||||
"github.com/micro/go-micro/v2/api/router"
|
||||
regRouter "github.com/micro/go-micro/v2/api/router/registry"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/micro/go-micro/v2/registry/memory"
|
||||
)
|
||||
|
||||
func testHttp(t *testing.T, path, service, ns string) {
|
||||
r := memory.NewRegistry()
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
s := ®istry.Service{
|
||||
Name: service,
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: service + "-1",
|
||||
Address: l.Addr().String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.Register(s)
|
||||
defer r.Deregister(s)
|
||||
|
||||
// setup the test handler
|
||||
m := http.NewServeMux()
|
||||
m.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`you got served`))
|
||||
})
|
||||
|
||||
// start http test serve
|
||||
go http.Serve(l, m)
|
||||
|
||||
// create new request and writer
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("POST", path, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// initialise the handler
|
||||
rt := regRouter.NewRouter(
|
||||
router.WithHandler("http"),
|
||||
router.WithRegistry(r),
|
||||
router.WithResolver(vpath.NewResolver(
|
||||
resolver.WithNamespace(resolver.StaticNamespace(ns)),
|
||||
)),
|
||||
)
|
||||
|
||||
p := NewHandler(handler.WithRouter(rt))
|
||||
|
||||
// execute the handler
|
||||
p.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("Expected 200 response got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
if w.Body.String() != "you got served" {
|
||||
t.Fatalf("Expected body: you got served. Got: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpHandler(t *testing.T) {
|
||||
testData := []struct {
|
||||
path string
|
||||
service string
|
||||
namespace string
|
||||
}{
|
||||
{
|
||||
"/test/foo",
|
||||
"go.micro.api.test",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/test/foo/baz",
|
||||
"go.micro.api.test",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v1/foo",
|
||||
"go.micro.api.v1.foo",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar",
|
||||
"go.micro.api.v1.foo",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v2/baz",
|
||||
"go.micro.api.v2.baz",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v2/baz/bar",
|
||||
"go.micro.api.v2.baz",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v2/baz/bar",
|
||||
"v2.baz",
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
t.Run(d.service, func(t *testing.T) {
|
||||
testHttp(t, d.path, d.service, d.namespace)
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,43 +1,44 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/unistack-org/micro/v3/api/router"
|
||||
"github.com/unistack-org/micro/v3/client"
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"github.com/micro/go-micro/v2/api/router"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
"github.com/micro/go-micro/v2/client/grpc"
|
||||
)
|
||||
|
||||
// DefaultMaxRecvSize specifies max recv size for handler
|
||||
var DefaultMaxRecvSize int64 = 1024 * 1024 * 100 // 10Mb
|
||||
var (
|
||||
DefaultMaxRecvSize int64 = 1024 * 1024 * 100 // 10Mb
|
||||
)
|
||||
|
||||
// Options struct holds handler options
|
||||
type Options struct {
|
||||
MaxRecvSize int64
|
||||
Namespace string
|
||||
Router router.Router
|
||||
Client client.Client
|
||||
Logger logger.Logger
|
||||
Namespace string
|
||||
MaxRecvSize int64
|
||||
}
|
||||
|
||||
// Option func signature
|
||||
type Option func(o *Options)
|
||||
|
||||
// NewOptions creates new options struct and fills it
|
||||
// NewOptions fills in the blanks
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Client: client.DefaultClient,
|
||||
Router: router.DefaultRouter,
|
||||
Logger: logger.DefaultLogger,
|
||||
MaxRecvSize: DefaultMaxRecvSize,
|
||||
}
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Client == nil {
|
||||
WithClient(grpc.NewClient())(&options)
|
||||
}
|
||||
|
||||
// set namespace if blank
|
||||
if len(options.Namespace) == 0 {
|
||||
WithNamespace("go.micro.api")(&options)
|
||||
}
|
||||
|
||||
if options.MaxRecvSize == 0 {
|
||||
options.MaxRecvSize = DefaultMaxRecvSize
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -55,14 +56,13 @@ func WithRouter(r router.Router) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithClient specifies client to be used by the handler
|
||||
func WithClient(c client.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.Client = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxRecvSize specifies max body size
|
||||
// WithmaxRecvSize specifies max body size
|
||||
func WithMaxRecvSize(size int64) Option {
|
||||
return func(o *Options) {
|
||||
o.MaxRecvSize = size
|
||||
|
522
api/handler/rpc/rpc.go
Normal file
522
api/handler/rpc/rpc.go
Normal file
@@ -0,0 +1,522 @@
|
||||
// Package rpc is a go-micro rpc handler.
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/api/internal/proto"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
"github.com/micro/go-micro/v2/codec"
|
||||
"github.com/micro/go-micro/v2/codec/jsonrpc"
|
||||
"github.com/micro/go-micro/v2/codec/protorpc"
|
||||
"github.com/micro/go-micro/v2/errors"
|
||||
"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/util/ctx"
|
||||
"github.com/micro/go-micro/v2/util/qson"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "rpc"
|
||||
)
|
||||
|
||||
var (
|
||||
// supported json codecs
|
||||
jsonCodecs = []string{
|
||||
"application/grpc+json",
|
||||
"application/json",
|
||||
"application/json-rpc",
|
||||
}
|
||||
|
||||
// support proto codecs
|
||||
protoCodecs = []string{
|
||||
"application/grpc",
|
||||
"application/grpc+proto",
|
||||
"application/proto",
|
||||
"application/protobuf",
|
||||
"application/proto-rpc",
|
||||
"application/octet-stream",
|
||||
}
|
||||
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
type rpcHandler struct {
|
||||
opts handler.Options
|
||||
s *api.Service
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func (b *buffer) Write(_ []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// strategy is a hack for selection
|
||||
func strategy(services []*registry.Service) selector.Strategy {
|
||||
return func(_ []*registry.Service) selector.Next {
|
||||
// ignore input to this function, use services above
|
||||
return selector.Random(services)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if h.opts.MaxRecvSize > 0 {
|
||||
bsize = h.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
|
||||
defer r.Body.Close()
|
||||
var service *api.Service
|
||||
|
||||
if h.s != nil {
|
||||
// we were given the service
|
||||
service = h.s
|
||||
} else if h.opts.Router != nil {
|
||||
// try get service from router
|
||||
s, err := h.opts.Router.Route(r)
|
||||
if err != nil {
|
||||
writeError(w, r, errors.InternalServerError("go.micro.api", err.Error()))
|
||||
return
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
writeError(w, r, errors.InternalServerError("go.micro.api", "no route found"))
|
||||
return
|
||||
}
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
|
||||
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
|
||||
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
||||
ct = ct[:idx]
|
||||
}
|
||||
|
||||
// micro client
|
||||
c := h.opts.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
|
||||
so := selector.WithStrategy(strategy(service.Services))
|
||||
|
||||
// walk the standard call path
|
||||
// get payload
|
||||
br, err := requestPayload(r)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var rsp []byte
|
||||
|
||||
switch {
|
||||
// proto codecs
|
||||
case hasCodec(ct, protoCodecs):
|
||||
request := &proto.Message{}
|
||||
// if the extracted payload isn't empty lets use it
|
||||
if len(br) > 0 {
|
||||
request = proto.NewMessage(br)
|
||||
}
|
||||
|
||||
// create request/response
|
||||
response := &proto.Message{}
|
||||
|
||||
req := c.NewRequest(
|
||||
service.Name,
|
||||
service.Endpoint.Name,
|
||||
request,
|
||||
client.WithContentType(ct),
|
||||
)
|
||||
|
||||
// make the call
|
||||
if err := c.Call(cx, req, response, client.WithSelectOption(so)); err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// marshall response
|
||||
rsp, err = response.Marshal()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
// if json codec is not present set to json
|
||||
if !hasCodec(ct, jsonCodecs) {
|
||||
ct = "application/json"
|
||||
}
|
||||
|
||||
// default to trying json
|
||||
var request json.RawMessage
|
||||
// if the extracted payload isn't empty lets use it
|
||||
if len(br) > 0 {
|
||||
request = json.RawMessage(br)
|
||||
}
|
||||
|
||||
// create request/response
|
||||
var response json.RawMessage
|
||||
|
||||
req := c.NewRequest(
|
||||
service.Name,
|
||||
service.Endpoint.Name,
|
||||
&request,
|
||||
client.WithContentType(ct),
|
||||
)
|
||||
// make the call
|
||||
if err := c.Call(cx, req, &response, client.WithSelectOption(so)); err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// marshall response
|
||||
rsp, err = response.MarshalJSON()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// write the response
|
||||
writeResponse(w, r, rsp)
|
||||
}
|
||||
|
||||
func (rh *rpcHandler) String() string {
|
||||
return "rpc"
|
||||
}
|
||||
|
||||
func hasCodec(ct string, codecs []string) bool {
|
||||
for _, codec := range codecs {
|
||||
if ct == codec {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// requestPayload takes a *http.Request.
|
||||
// If the request is a GET the query string parameters are extracted and marshaled to JSON and the raw bytes are returned.
|
||||
// If the request method is a POST the request body is read and returned
|
||||
func requestPayload(r *http.Request) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
// we have to decode json-rpc and proto-rpc because we suck
|
||||
// well actually because there's no proxy codec right now
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
switch {
|
||||
case strings.Contains(ct, "application/json-rpc"):
|
||||
msg := codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
c := jsonrpc.NewCodec(&buffer{r.Body})
|
||||
if err = c.ReadHeader(&msg, codec.Request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw json.RawMessage
|
||||
if err = c.ReadBody(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ([]byte)(raw), nil
|
||||
case strings.Contains(ct, "application/proto-rpc"), strings.Contains(ct, "application/octet-stream"):
|
||||
msg := codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
c := protorpc.NewCodec(&buffer{r.Body})
|
||||
if err = c.ReadHeader(&msg, codec.Request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw proto.Message
|
||||
if err = c.ReadBody(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw.Marshal()
|
||||
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
|
||||
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 {
|
||||
case "GET":
|
||||
// empty response
|
||||
if strings.Contains(ct, "application/json") && string(out) == "{}" {
|
||||
return out, nil
|
||||
} else if string(out) == "{}" && !strings.Contains(ct, "application/json") {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return out, nil
|
||||
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
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
ce := errors.Parse(err.Error())
|
||||
|
||||
switch ce.Code {
|
||||
case 0:
|
||||
// assuming it's totally screwed
|
||||
ce.Code = 500
|
||||
ce.Id = "go.micro.api"
|
||||
ce.Status = http.StatusText(500)
|
||||
ce.Detail = "error during request: " + ce.Detail
|
||||
w.WriteHeader(500)
|
||||
default:
|
||||
w.WriteHeader(int(ce.Code))
|
||||
}
|
||||
|
||||
// response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Set trailers
|
||||
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
|
||||
w.Header().Set("Trailer", "grpc-status")
|
||||
w.Header().Set("Trailer", "grpc-message")
|
||||
w.Header().Set("grpc-status", "13")
|
||||
w.Header().Set("grpc-message", ce.Detail)
|
||||
}
|
||||
|
||||
_, 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) {
|
||||
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(rsp)))
|
||||
|
||||
// Set trailers
|
||||
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
|
||||
w.Header().Set("Trailer", "grpc-status")
|
||||
w.Header().Set("Trailer", "grpc-message")
|
||||
w.Header().Set("grpc-status", "0")
|
||||
w.Header().Set("grpc-message", "")
|
||||
}
|
||||
|
||||
// write 204 status if rsp is nil
|
||||
if len(rsp) == 0 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// write response
|
||||
_, err := w.Write(rsp)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &rpcHandler{
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &rpcHandler{
|
||||
opts: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
112
api/handler/rpc/rpc_test.go
Normal file
112
api/handler/rpc/rpc_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
go_api "github.com/micro/go-micro/v2/api/proto"
|
||||
)
|
||||
|
||||
func TestRequestPayloadFromRequest(t *testing.T) {
|
||||
|
||||
// our test event so that we can validate serialising / deserializing of true protos works
|
||||
protoEvent := go_api.Event{
|
||||
Name: "Test",
|
||||
}
|
||||
|
||||
protoBytes, err := proto.Marshal(&protoEvent)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to marshal proto", err)
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(protoEvent)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to marshal proto to JSON ", err)
|
||||
}
|
||||
|
||||
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) {
|
||||
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(protoBytes))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != string(protoBytes) {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), string(protoBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extracting JSON from a POST request", func(t *testing.T) {
|
||||
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(jsonBytes))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != string(jsonBytes) {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extracting params from a GET request", func(t *testing.T) {
|
||||
|
||||
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
q.Add("name", "Test")
|
||||
r.URL.RawQuery = q.Encode()
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != string(jsonBytes) {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET request with no params", func(t *testing.T) {
|
||||
|
||||
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != "" {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), "")
|
||||
}
|
||||
})
|
||||
}
|
259
api/handler/rpc/stream.go
Normal file
259
api/handler/rpc/stream.go
Normal file
@@ -0,0 +1,259 @@
|
||||
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.StatusGoingAway:
|
||||
// this happens when user leave the page
|
||||
return
|
||||
case ws.StatusNormalClosure, ws.StatusNoStatusRcvd:
|
||||
// this happens when user close ws connection, or we don't get any status
|
||||
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
|
||||
}
|
177
api/handler/web/web.go
Normal file
177
api/handler/web/web.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Package web contains the web handler including websocket support
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "web"
|
||||
)
|
||||
|
||||
type webHandler struct {
|
||||
opts handler.Options
|
||||
s *api.Service
|
||||
}
|
||||
|
||||
func (wh *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := wh.getService(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
if len(service) == 0 {
|
||||
w.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
|
||||
rp, err := url.Parse(service)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
if isWebSocket(r) {
|
||||
wh.serveWebSocket(rp.Host, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getService returns the service for this request from the selector
|
||||
func (wh *webHandler) getService(r *http.Request) (string, error) {
|
||||
var service *api.Service
|
||||
|
||||
if wh.s != nil {
|
||||
// we were given the service
|
||||
service = wh.s
|
||||
} else if wh.opts.Router != nil {
|
||||
// try get service from router
|
||||
s, err := wh.opts.Router.Route(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
return "", errors.New("no route found")
|
||||
}
|
||||
|
||||
// create a random selector
|
||||
next := selector.Random(service.Services)
|
||||
|
||||
// get the next node
|
||||
s, err := next()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s", s.Address), nil
|
||||
}
|
||||
|
||||
// serveWebSocket used to serve a web socket proxied connection
|
||||
func (wh *webHandler) serveWebSocket(host string, w http.ResponseWriter, r *http.Request) {
|
||||
req := new(http.Request)
|
||||
*req = *r
|
||||
|
||||
if len(host) == 0 {
|
||||
http.Error(w, "invalid host", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// set x-forward-for
|
||||
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if ips, ok := req.Header["X-Forwarded-For"]; ok {
|
||||
clientIP = strings.Join(ips, ", ") + ", " + clientIP
|
||||
}
|
||||
req.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
|
||||
// connect to the backend host
|
||||
conn, err := net.Dial("tcp", host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// hijack the connection
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "failed to connect", 500)
|
||||
return
|
||||
}
|
||||
|
||||
nc, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer nc.Close()
|
||||
defer conn.Close()
|
||||
|
||||
if err = req.Write(conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
cp := func(dst io.Writer, src io.Reader) {
|
||||
_, err := io.Copy(dst, src)
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
go cp(conn, nc)
|
||||
go cp(nc, conn)
|
||||
|
||||
<-errCh
|
||||
}
|
||||
|
||||
func isWebSocket(r *http.Request) bool {
|
||||
contains := func(key, val string) bool {
|
||||
vv := strings.Split(r.Header.Get(key), ",")
|
||||
for _, v := range vv {
|
||||
if val == strings.ToLower(strings.TrimSpace(v)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if contains("Connection", "upgrade") && contains("Upgrade", "websocket") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (wh *webHandler) String() string {
|
||||
return "web"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
return &webHandler{
|
||||
opts: handler.NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
|
||||
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return &webHandler{
|
||||
opts: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
28
api/internal/proto/message.pb.go
Normal file
28
api/internal/proto/message.pb.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package proto
|
||||
|
||||
type Message struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (m *Message) ProtoMessage() {}
|
||||
|
||||
func (m *Message) Reset() {
|
||||
*m = Message{}
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
return string(m.data)
|
||||
}
|
||||
|
||||
func (m *Message) Marshal() ([]byte, error) {
|
||||
return m.data, nil
|
||||
}
|
||||
|
||||
func (m *Message) Unmarshal(data []byte) error {
|
||||
m.data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMessage(data []byte) *Message {
|
||||
return &Message{data}
|
||||
}
|
335
api/proto/api.pb.go
Normal file
335
api/proto/api.pb.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: api/proto/api.proto
|
||||
|
||||
package go_api
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
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 Pair struct {
|
||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Pair) Reset() { *m = Pair{} }
|
||||
func (m *Pair) String() string { return proto.CompactTextString(m) }
|
||||
func (*Pair) ProtoMessage() {}
|
||||
func (*Pair) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{0}
|
||||
}
|
||||
|
||||
func (m *Pair) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Pair.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Pair) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Pair.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Pair) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Pair.Merge(m, src)
|
||||
}
|
||||
func (m *Pair) XXX_Size() int {
|
||||
return xxx_messageInfo_Pair.Size(m)
|
||||
}
|
||||
func (m *Pair) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Pair.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Pair proto.InternalMessageInfo
|
||||
|
||||
func (m *Pair) GetKey() string {
|
||||
if m != nil {
|
||||
return m.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Pair) GetValues() []string {
|
||||
if m != nil {
|
||||
return m.Values
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A HTTP request as RPC
|
||||
// Forward by the api handler
|
||||
type Request struct {
|
||||
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"`
|
||||
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
|
||||
Header map[string]*Pair `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Get map[string]*Pair `protobuf:"bytes,4,rep,name=get,proto3" json:"get,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Post map[string]*Pair `protobuf:"bytes,5,rep,name=post,proto3" json:"post,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
|
||||
Url string `protobuf:"bytes,7,opt,name=url,proto3" json:"url,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Request) Reset() { *m = Request{} }
|
||||
func (m *Request) String() string { return proto.CompactTextString(m) }
|
||||
func (*Request) ProtoMessage() {}
|
||||
func (*Request) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{1}
|
||||
}
|
||||
|
||||
func (m *Request) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Request.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Request) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Request.Merge(m, src)
|
||||
}
|
||||
func (m *Request) XXX_Size() int {
|
||||
return xxx_messageInfo_Request.Size(m)
|
||||
}
|
||||
func (m *Request) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Request.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Request proto.InternalMessageInfo
|
||||
|
||||
func (m *Request) GetMethod() string {
|
||||
if m != nil {
|
||||
return m.Method
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetPath() string {
|
||||
if m != nil {
|
||||
return m.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetHeader() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetGet() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Get
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetPost() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Post
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetBody() string {
|
||||
if m != nil {
|
||||
return m.Body
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetUrl() string {
|
||||
if m != nil {
|
||||
return m.Url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A HTTP response as RPC
|
||||
// Expected response for the api handler
|
||||
type Response struct {
|
||||
StatusCode int32 `protobuf:"varint,1,opt,name=statusCode,proto3" json:"statusCode,omitempty"`
|
||||
Header map[string]*Pair `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Response) Reset() { *m = Response{} }
|
||||
func (m *Response) String() string { return proto.CompactTextString(m) }
|
||||
func (*Response) ProtoMessage() {}
|
||||
func (*Response) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{2}
|
||||
}
|
||||
|
||||
func (m *Response) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Response.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Response) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Response.Merge(m, src)
|
||||
}
|
||||
func (m *Response) XXX_Size() int {
|
||||
return xxx_messageInfo_Response.Size(m)
|
||||
}
|
||||
func (m *Response) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Response.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Response proto.InternalMessageInfo
|
||||
|
||||
func (m *Response) GetStatusCode() int32 {
|
||||
if m != nil {
|
||||
return m.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Response) GetHeader() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Response) GetBody() string {
|
||||
if m != nil {
|
||||
return m.Body
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A HTTP event as RPC
|
||||
// Forwarded by the event handler
|
||||
type Event struct {
|
||||
// e.g login
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// uuid
|
||||
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
// unix timestamp of event
|
||||
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
// event headers
|
||||
Header map[string]*Pair `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
// the event data
|
||||
Data string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Event) Reset() { *m = Event{} }
|
||||
func (m *Event) String() string { return proto.CompactTextString(m) }
|
||||
func (*Event) ProtoMessage() {}
|
||||
func (*Event) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{3}
|
||||
}
|
||||
|
||||
func (m *Event) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Event.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Event.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Event) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Event.Merge(m, src)
|
||||
}
|
||||
func (m *Event) XXX_Size() int {
|
||||
return xxx_messageInfo_Event.Size(m)
|
||||
}
|
||||
func (m *Event) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Event.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Event proto.InternalMessageInfo
|
||||
|
||||
func (m *Event) GetName() string {
|
||||
if m != nil {
|
||||
return m.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Event) GetId() string {
|
||||
if m != nil {
|
||||
return m.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Event) GetTimestamp() int64 {
|
||||
if m != nil {
|
||||
return m.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Event) GetHeader() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Event) GetData() string {
|
||||
if m != nil {
|
||||
return m.Data
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Pair)(nil), "go.api.Pair")
|
||||
proto.RegisterType((*Request)(nil), "go.api.Request")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.GetEntry")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.HeaderEntry")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.PostEntry")
|
||||
proto.RegisterType((*Response)(nil), "go.api.Response")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Response.HeaderEntry")
|
||||
proto.RegisterType((*Event)(nil), "go.api.Event")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Event.HeaderEntry")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("api/proto/api.proto", fileDescriptor_2df576b66d12087a) }
|
||||
|
||||
var fileDescriptor_2df576b66d12087a = []byte{
|
||||
// 393 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xcd, 0xce, 0xd3, 0x30,
|
||||
0x10, 0x54, 0xe2, 0x24, 0x6d, 0xb6, 0x08, 0x21, 0x23, 0x21, 0x53, 0x2a, 0x54, 0xe5, 0x54, 0x21,
|
||||
0x91, 0x42, 0xcb, 0x01, 0x71, 0x85, 0xaa, 0x1c, 0x2b, 0xbf, 0x81, 0xab, 0x58, 0x6d, 0x44, 0x13,
|
||||
0x9b, 0xd8, 0xa9, 0xd4, 0x87, 0xe3, 0xc0, 0x63, 0xf0, 0x36, 0xc8, 0x1b, 0xf7, 0xe7, 0xab, 0xfa,
|
||||
0x5d, 0xbe, 0xaf, 0xb7, 0x89, 0x3d, 0x3b, 0x3b, 0x3b, 0xeb, 0xc0, 0x6b, 0xa1, 0xcb, 0xa9, 0x6e,
|
||||
0x94, 0x55, 0x53, 0xa1, 0xcb, 0x1c, 0x11, 0x4d, 0x36, 0x2a, 0x17, 0xba, 0xcc, 0x3e, 0x41, 0xb4,
|
||||
0x12, 0x65, 0x43, 0x5f, 0x01, 0xf9, 0x25, 0x0f, 0x2c, 0x18, 0x07, 0x93, 0x94, 0x3b, 0x48, 0xdf,
|
||||
0x40, 0xb2, 0x17, 0xbb, 0x56, 0x1a, 0x16, 0x8e, 0xc9, 0x24, 0xe5, 0xfe, 0x2b, 0xfb, 0x4b, 0xa0,
|
||||
0xc7, 0xe5, 0xef, 0x56, 0x1a, 0xeb, 0x38, 0x95, 0xb4, 0x5b, 0x55, 0xf8, 0x42, 0xff, 0x45, 0x29,
|
||||
0x44, 0x5a, 0xd8, 0x2d, 0x0b, 0xf1, 0x14, 0x31, 0x9d, 0x43, 0xb2, 0x95, 0xa2, 0x90, 0x0d, 0x23,
|
||||
0x63, 0x32, 0x19, 0xcc, 0xde, 0xe5, 0x9d, 0x85, 0xdc, 0x8b, 0xe5, 0x3f, 0xf1, 0x76, 0x51, 0xdb,
|
||||
0xe6, 0xc0, 0x3d, 0x95, 0x7e, 0x00, 0xb2, 0x91, 0x96, 0x45, 0x58, 0xc1, 0xae, 0x2b, 0x96, 0xd2,
|
||||
0x76, 0x74, 0x47, 0xa2, 0x1f, 0x21, 0xd2, 0xca, 0x58, 0x16, 0x23, 0xf9, 0xed, 0x35, 0x79, 0xa5,
|
||||
0x8c, 0x67, 0x23, 0xcd, 0x79, 0x5c, 0xab, 0xe2, 0xc0, 0x92, 0xce, 0xa3, 0xc3, 0x2e, 0x85, 0xb6,
|
||||
0xd9, 0xb1, 0x5e, 0x97, 0x42, 0xdb, 0xec, 0x86, 0x4b, 0x18, 0x5c, 0xf8, 0xba, 0x11, 0x53, 0x06,
|
||||
0x31, 0x06, 0x83, 0xb3, 0x0e, 0x66, 0x2f, 0x8e, 0x6d, 0x5d, 0xaa, 0xbc, 0xbb, 0xfa, 0x16, 0x7e,
|
||||
0x0d, 0x86, 0x3f, 0xa0, 0x7f, 0xb4, 0xfb, 0x0c, 0x95, 0x05, 0xa4, 0xa7, 0x39, 0x9e, 0x2e, 0x93,
|
||||
0xfd, 0x09, 0xa0, 0xcf, 0xa5, 0xd1, 0xaa, 0x36, 0x92, 0xbe, 0x07, 0x30, 0x56, 0xd8, 0xd6, 0x7c,
|
||||
0x57, 0x85, 0x44, 0xb5, 0x98, 0x5f, 0x9c, 0xd0, 0x2f, 0xa7, 0xc5, 0x85, 0x98, 0xec, 0xe8, 0x9c,
|
||||
0x6c, 0xa7, 0x70, 0x73, 0x73, 0xc7, 0x78, 0xc9, 0x39, 0xde, 0xbb, 0x85, 0x99, 0xfd, 0x0b, 0x20,
|
||||
0x5e, 0xec, 0x65, 0x8d, 0x5b, 0xac, 0x45, 0x25, 0xbd, 0x08, 0x62, 0xfa, 0x12, 0xc2, 0xb2, 0xf0,
|
||||
0x6f, 0x2f, 0x2c, 0x0b, 0x3a, 0x82, 0xd4, 0x96, 0x95, 0x34, 0x56, 0x54, 0x1a, 0xfd, 0x10, 0x7e,
|
||||
0x3e, 0xa0, 0x9f, 0x4f, 0xe3, 0x45, 0x0f, 0x1f, 0x0e, 0x36, 0x78, 0x6c, 0xb6, 0x42, 0x58, 0xc1,
|
||||
0xe2, 0xae, 0xa9, 0xc3, 0x77, 0x9b, 0x6d, 0x9d, 0xe0, 0x0f, 0x3a, 0xff, 0x1f, 0x00, 0x00, 0xff,
|
||||
0xff, 0xd4, 0x6d, 0x70, 0x51, 0xb7, 0x03, 0x00, 0x00,
|
||||
}
|
21
api/proto/api.pb.micro.go
Normal file
21
api/proto/api.pb.micro.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: api/proto/api.proto
|
||||
|
||||
package go_api
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
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
|
43
api/proto/api.proto
Normal file
43
api/proto/api.proto
Normal file
@@ -0,0 +1,43 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package go.api;
|
||||
|
||||
message Pair {
|
||||
string key = 1;
|
||||
repeated string values = 2;
|
||||
}
|
||||
|
||||
// A HTTP request as RPC
|
||||
// Forward by the api handler
|
||||
message Request {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
map<string, Pair> header = 3;
|
||||
map<string, Pair> get = 4;
|
||||
map<string, Pair> post = 5;
|
||||
string body = 6; // raw request body; if not application/x-www-form-urlencoded
|
||||
string url = 7;
|
||||
}
|
||||
|
||||
// A HTTP response as RPC
|
||||
// Expected response for the api handler
|
||||
message Response {
|
||||
int32 statusCode = 1;
|
||||
map<string, Pair> header = 2;
|
||||
string body = 3;
|
||||
}
|
||||
|
||||
// A HTTP event as RPC
|
||||
// Forwarded by the event handler
|
||||
message Event {
|
||||
// e.g login
|
||||
string name = 1;
|
||||
// uuid
|
||||
string id = 2;
|
||||
// unix timestamp of event
|
||||
int64 timestamp = 3;
|
||||
// event headers
|
||||
map<string, Pair> header = 4;
|
||||
// the event data
|
||||
string data = 5;
|
||||
}
|
@@ -6,19 +6,12 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
// Resolver struct
|
||||
type Resolver struct {
|
||||
opts resolver.Options
|
||||
}
|
||||
|
||||
// Resolve func to resolve enndpoint
|
||||
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
||||
// parse options
|
||||
options := resolver.NewResolveOptions(opts...)
|
||||
type Resolver struct{}
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
// /foo.Bar/Service
|
||||
if req.URL.Path == "/" {
|
||||
return nil, errors.New("unknown name")
|
||||
@@ -33,7 +26,6 @@ func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: options.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -41,7 +33,6 @@ func (r *Resolver) String() string {
|
||||
return "grpc"
|
||||
}
|
||||
|
||||
// NewResolver is used to create new Resolver
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{opts: resolver.NewOptions(opts...)}
|
||||
return &Resolver{}
|
||||
}
|
||||
|
@@ -4,32 +4,26 @@ package host
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
type hostResolver struct {
|
||||
type Resolver struct {
|
||||
opts resolver.Options
|
||||
}
|
||||
|
||||
// Resolve endpoint
|
||||
func (r *hostResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
||||
// parse options
|
||||
options := resolver.NewResolveOptions(opts...)
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
return &resolver.Endpoint{
|
||||
Name: req.Host,
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: options.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *hostResolver) String() string {
|
||||
func (r *Resolver) String() string {
|
||||
return "host"
|
||||
}
|
||||
|
||||
// NewResolver creates new host api resolver
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &hostResolver{opts: resolver.NewOptions(opts...)}
|
||||
return &Resolver{opts: resolver.NewOptions(opts...)}
|
||||
}
|
||||
|
@@ -1,23 +1,22 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/unistack-org/micro/v3/register"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Options struct
|
||||
type Options struct {
|
||||
// Context is for external defined options
|
||||
Context context.Context
|
||||
// Handler name
|
||||
Handler string
|
||||
// ServicePrefix is the prefix
|
||||
ServicePrefix string
|
||||
}
|
||||
// NewOptions returns new initialised options
|
||||
func NewOptions(opts ...Option) Options {
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// Option func
|
||||
type Option func(o *Options)
|
||||
if options.Namespace == nil {
|
||||
options.Namespace = StaticNamespace("go.micro")
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithHandler sets the handler being used
|
||||
func WithHandler(h string) Option {
|
||||
@@ -26,45 +25,9 @@ func WithHandler(h string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithServicePrefix sets the ServicePrefix option
|
||||
func WithServicePrefix(p string) Option {
|
||||
// WithNamespace sets the function which determines the namespace for a request
|
||||
func WithNamespace(n func(*http.Request) string) Option {
|
||||
return func(o *Options) {
|
||||
o.ServicePrefix = p
|
||||
o.Namespace = n
|
||||
}
|
||||
}
|
||||
|
||||
// NewOptions returns new initialised options
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Context: context.Background(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// ResolveOptions are used when resolving a request
|
||||
type ResolveOptions struct {
|
||||
Domain string
|
||||
}
|
||||
|
||||
// ResolveOption sets an option
|
||||
type ResolveOption func(*ResolveOptions)
|
||||
|
||||
// Domain sets the resolve Domain option
|
||||
func Domain(n string) ResolveOption {
|
||||
return func(o *ResolveOptions) {
|
||||
o.Domain = n
|
||||
}
|
||||
}
|
||||
|
||||
// NewResolveOptions returns new initialised resolve options
|
||||
func NewResolveOptions(opts ...ResolveOption) ResolveOptions {
|
||||
options := ResolveOptions{Domain: register.DefaultDomain}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
@@ -5,40 +5,33 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
// Resolver the path resolver
|
||||
type Resolver struct {
|
||||
opts resolver.Options
|
||||
}
|
||||
|
||||
// Resolve resolves endpoint
|
||||
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
||||
// parse options
|
||||
options := resolver.NewResolveOptions(opts...)
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
if req.URL.Path == "/" {
|
||||
return nil, resolver.ErrNotFound
|
||||
}
|
||||
|
||||
parts := strings.Split(req.URL.Path[1:], "/")
|
||||
ns := r.opts.Namespace(req)
|
||||
|
||||
return &resolver.Endpoint{
|
||||
Name: r.opts.ServicePrefix + "." + parts[0],
|
||||
Name: ns + "." + parts[0],
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: options.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String retruns the string representation
|
||||
func (r *Resolver) String() string {
|
||||
return "path"
|
||||
}
|
||||
|
||||
// NewResolver returns new path resolver
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{opts: resolver.NewOptions(opts...)}
|
||||
}
|
||||
|
@@ -7,21 +7,19 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound returned when endpoint is not found
|
||||
ErrNotFound = errors.New("not found")
|
||||
// ErrInvalidPath returned on invalid path
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrInvalidPath = errors.New("invalid path")
|
||||
)
|
||||
|
||||
// Resolver resolves requests to endpoints
|
||||
type Resolver interface {
|
||||
Resolve(r *http.Request, opts ...ResolveOption) (*Endpoint, error)
|
||||
Resolve(r *http.Request) (*Endpoint, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// Endpoint is the endpoint for a http request
|
||||
type Endpoint struct {
|
||||
// Endpoint name e.g greeter
|
||||
// e.g greeter
|
||||
Name string
|
||||
// HTTP Host e.g example.com
|
||||
Host string
|
||||
@@ -29,6 +27,18 @@ type Endpoint struct {
|
||||
Method string
|
||||
// HTTP Path e.g /greeter.
|
||||
Path string
|
||||
// Domain endpoint exists within
|
||||
Domain string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Handler string
|
||||
Namespace func(*http.Request) string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@@ -1,90 +0,0 @@
|
||||
// Package subdomain is a resolver which uses the subdomain to determine the domain to route to. It
|
||||
// offloads the endpoint resolution to a child resolver which is provided in New.
|
||||
package subdomain
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api/resolver"
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
// NewResolver creates new subdomain api resolver
|
||||
func NewResolver(parent resolver.Resolver, opts ...resolver.Option) resolver.Resolver {
|
||||
options := resolver.NewOptions(opts...)
|
||||
return &subdomainResolver{opts: options, Resolver: parent}
|
||||
}
|
||||
|
||||
type subdomainResolver struct {
|
||||
resolver.Resolver
|
||||
opts resolver.Options
|
||||
}
|
||||
|
||||
// Resolve resolve endpoint based on subdomain
|
||||
func (r *subdomainResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
||||
if dom := r.Domain(req); len(dom) > 0 {
|
||||
opts = append(opts, resolver.Domain(dom))
|
||||
}
|
||||
|
||||
return r.Resolver.Resolve(req, opts...)
|
||||
}
|
||||
|
||||
// Domain returns domain
|
||||
func (r *subdomainResolver) Domain(req *http.Request) string {
|
||||
// determine the host, e.g. foobar.m3o.app
|
||||
host := req.URL.Hostname()
|
||||
if len(host) == 0 {
|
||||
if h, _, err := net.SplitHostPort(req.Host); err == nil {
|
||||
host = h // host does contain a port
|
||||
} else if strings.Contains(err.Error(), "missing port in address") {
|
||||
host = req.Host // host does not contain a port
|
||||
}
|
||||
}
|
||||
|
||||
// check for an ip address
|
||||
if net.ParseIP(host) != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// check for dev environment
|
||||
if host == "localhost" || host == "127.0.0.1" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// extract the top level domain plus one (e.g. 'myapp.com')
|
||||
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
|
||||
if err != nil {
|
||||
if logger.V(logger.DebugLevel) {
|
||||
logger.Debug(r.opts.Context, "Unable to extract domain from %v", host)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// there was no subdomain
|
||||
if host == domain {
|
||||
return ""
|
||||
}
|
||||
|
||||
// remove the domain from the host, leaving the subdomain, e.g. "staging.foo.myapp.com" => "staging.foo"
|
||||
subdomain := strings.TrimSuffix(host, "."+domain)
|
||||
|
||||
// ignore the API subdomain
|
||||
if subdomain == "api" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// return the reversed subdomain as the namespace, e.g. "staging.foo" => "foo-staging"
|
||||
comps := strings.Split(subdomain, ".")
|
||||
for i := len(comps)/2 - 1; i >= 0; i-- {
|
||||
opp := len(comps) - 1 - i
|
||||
comps[i], comps[opp] = comps[opp], comps[i]
|
||||
}
|
||||
return strings.Join(comps, "-")
|
||||
}
|
||||
|
||||
func (r *subdomainResolver) String() string {
|
||||
return "subdomain"
|
||||
}
|
@@ -1,73 +0,0 @@
|
||||
package subdomain
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api/resolver/vpath"
|
||||
)
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
tt := []struct {
|
||||
Name string
|
||||
Host string
|
||||
Result string
|
||||
}{
|
||||
{
|
||||
Name: "Top level domain",
|
||||
Host: "micro.mu",
|
||||
Result: "micro",
|
||||
},
|
||||
{
|
||||
Name: "Effective top level domain",
|
||||
Host: "micro.com.au",
|
||||
Result: "micro",
|
||||
},
|
||||
{
|
||||
Name: "Subdomain dev",
|
||||
Host: "dev.micro.mu",
|
||||
Result: "dev",
|
||||
},
|
||||
{
|
||||
Name: "Subdomain foo",
|
||||
Host: "foo.micro.mu",
|
||||
Result: "foo",
|
||||
},
|
||||
{
|
||||
Name: "Multi-level subdomain",
|
||||
Host: "staging.myapp.m3o.app",
|
||||
Result: "myapp-staging",
|
||||
},
|
||||
{
|
||||
Name: "Dev host",
|
||||
Host: "127.0.0.1",
|
||||
Result: "micro",
|
||||
},
|
||||
{
|
||||
Name: "Localhost",
|
||||
Host: "localhost",
|
||||
Result: "micro",
|
||||
},
|
||||
{
|
||||
Name: "IP host",
|
||||
Host: "81.151.101.146",
|
||||
Result: "micro",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
r := NewResolver(vpath.NewResolver())
|
||||
result, err := r.Resolve(&http.Request{URL: &url.URL{Host: tc.Host, Path: "foo/bar"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result != nil {
|
||||
if tc.Result != result.Domain {
|
||||
t.Fatalf("Expected %v but got %v", tc.Result, result.Domain)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -7,69 +7,63 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
// NewResolver creates new vpath api resolver
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &vpathResolver{opts: resolver.NewOptions(opts...)}
|
||||
return &Resolver{opts: resolver.NewOptions(opts...)}
|
||||
}
|
||||
|
||||
type vpathResolver struct {
|
||||
type Resolver struct {
|
||||
opts resolver.Options
|
||||
}
|
||||
|
||||
var re = regexp.MustCompile("^v[0-9]+$")
|
||||
var (
|
||||
re = regexp.MustCompile("^v[0-9]+$")
|
||||
)
|
||||
|
||||
// Resolve endpoint
|
||||
func (r *vpathResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
if req.URL.Path == "/" {
|
||||
return nil, errors.New("unknown name")
|
||||
}
|
||||
|
||||
options := resolver.NewResolveOptions(opts...)
|
||||
|
||||
parts := strings.Split(req.URL.Path[1:], "/")
|
||||
if len(parts) == 1 {
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withPrefix(parts...),
|
||||
Name: r.withNamespace(req, parts...),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: options.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// /v1/foo
|
||||
if re.MatchString(parts[0]) {
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withPrefix(parts[0:2]...),
|
||||
Name: r.withNamespace(req, parts[0:2]...),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: options.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withPrefix(parts[0]),
|
||||
Name: r.withNamespace(req, parts[0]),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: options.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *vpathResolver) String() string {
|
||||
return "vpath"
|
||||
func (r *Resolver) String() string {
|
||||
return "path"
|
||||
}
|
||||
|
||||
// withPrefix transforms "foo" into "go.micro.api.foo"
|
||||
func (r *vpathResolver) withPrefix(parts ...string) string {
|
||||
p := r.opts.ServicePrefix
|
||||
if len(p) > 0 {
|
||||
parts = append([]string{p}, parts...)
|
||||
func (r *Resolver) withNamespace(req *http.Request, parts ...string) string {
|
||||
ns := r.opts.Namespace(req)
|
||||
if len(ns) == 0 {
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
return strings.Join(parts, ".")
|
||||
return strings.Join(append([]string{ns}, parts...), ".")
|
||||
}
|
||||
|
@@ -1,36 +1,23 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api/resolver"
|
||||
"github.com/unistack-org/micro/v3/api/resolver/vpath"
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"github.com/unistack-org/micro/v3/register"
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/resolver/vpath"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
)
|
||||
|
||||
// Options holds the options for api router
|
||||
type Options struct {
|
||||
// Register for service lookup
|
||||
Register register.Register
|
||||
// Resolver to use
|
||||
Handler string
|
||||
Registry registry.Registry
|
||||
Resolver resolver.Resolver
|
||||
// Logger micro logger
|
||||
Logger logger.Logger
|
||||
// Context is for external options
|
||||
Context context.Context
|
||||
// Handler name
|
||||
Handler string
|
||||
}
|
||||
|
||||
// Option func signature
|
||||
type Option func(o *Options)
|
||||
|
||||
// NewOptions returns options struct filled by opts
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Context: context.Background(),
|
||||
Handler: "meta",
|
||||
Handler: "meta",
|
||||
Registry: registry.DefaultRegistry,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
@@ -46,28 +33,18 @@ func NewOptions(opts ...Option) Options {
|
||||
return options
|
||||
}
|
||||
|
||||
// WithContext sets the context
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithHandler sets the handler
|
||||
func WithHandler(h string) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = h
|
||||
}
|
||||
}
|
||||
|
||||
// WithRegister sets the register
|
||||
func WithRegister(r register.Register) Option {
|
||||
func WithRegistry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Register = r
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithResolver sets the resolver
|
||||
func WithResolver(r resolver.Resolver) Option {
|
||||
return func(o *Options) {
|
||||
o.Resolver = r
|
||||
|
498
api/router/registry/registry.go
Normal file
498
api/router/registry/registry.go
Normal file
@@ -0,0 +1,498 @@
|
||||
// Package registry provides a dynamic api service router
|
||||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"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
|
||||
type registryRouter struct {
|
||||
exit chan bool
|
||||
opts router.Options
|
||||
|
||||
// registry cache
|
||||
rc cache.Cache
|
||||
|
||||
sync.RWMutex
|
||||
eps map[string]*api.Service
|
||||
// compiled regexp for host and path
|
||||
ceps map[string]*endpoint
|
||||
}
|
||||
|
||||
func (r *registryRouter) isClosed() bool {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// refresh list of api services
|
||||
func (r *registryRouter) refresh() {
|
||||
var attempts int
|
||||
|
||||
for {
|
||||
services, err := r.opts.Registry.ListServices()
|
||||
if err != nil {
|
||||
attempts++
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("unable to list services: %v", err)
|
||||
}
|
||||
time.Sleep(time.Duration(attempts) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
attempts = 0
|
||||
|
||||
// for each service, get service and store endpoints
|
||||
for _, s := range services {
|
||||
service, err := r.rc.GetService(s.Name)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("unable to get service: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
r.store(service)
|
||||
}
|
||||
|
||||
// refresh list in 10 minutes... cruft
|
||||
// use registry watching
|
||||
select {
|
||||
case <-time.After(time.Minute * 10):
|
||||
case <-r.exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process watch event
|
||||
func (r *registryRouter) process(res *registry.Result) {
|
||||
// skip these things
|
||||
if res == nil || res.Service == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// get entry from cache
|
||||
service, err := r.rc.GetService(res.Service.Name)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("unable to get service: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// update our local endpoints
|
||||
r.store(service)
|
||||
}
|
||||
|
||||
// store local endpoint cache
|
||||
func (r *registryRouter) store(services []*registry.Service) {
|
||||
// endpoints
|
||||
eps := map[string]*api.Service{}
|
||||
|
||||
// services
|
||||
names := map[string]bool{}
|
||||
|
||||
// create a new endpoint mapping
|
||||
for _, service := range services {
|
||||
// set names we need later
|
||||
names[service.Name] = true
|
||||
|
||||
// map per endpoint
|
||||
for _, sep := range service.Endpoints {
|
||||
// create a key service:endpoint_name
|
||||
key := fmt.Sprintf("%s.%s", service.Name, sep.Name)
|
||||
// decode endpoint
|
||||
end := api.Decode(sep.Metadata)
|
||||
|
||||
// if we got nothing skip
|
||||
if err := api.Validate(end); err != nil {
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("endpoint validation failed: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// try get endpoint
|
||||
ep, ok := eps[key]
|
||||
if !ok {
|
||||
ep = &api.Service{Name: service.Name}
|
||||
}
|
||||
|
||||
// overwrite the endpoint
|
||||
ep.Endpoint = end
|
||||
// append services
|
||||
ep.Services = append(ep.Services, service)
|
||||
// store it
|
||||
eps[key] = ep
|
||||
}
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// delete any existing eps for services we know
|
||||
for key, service := range r.eps {
|
||||
// skip what we don't care about
|
||||
if !names[service.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
// ok we know this thing
|
||||
// delete delete delete
|
||||
delete(r.eps, key)
|
||||
}
|
||||
|
||||
// now set the eps we have
|
||||
for name, ep := range eps {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// watch for endpoint changes
|
||||
func (r *registryRouter) watch() {
|
||||
var attempts int
|
||||
|
||||
for {
|
||||
if r.isClosed() {
|
||||
return
|
||||
}
|
||||
|
||||
// watch for changes
|
||||
w, err := r.opts.Registry.Watch()
|
||||
if err != nil {
|
||||
attempts++
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("error watching endpoints: %v", 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 {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("error getting next endoint: %v", err)
|
||||
}
|
||||
close(ch)
|
||||
break
|
||||
}
|
||||
r.process(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registryRouter) Options() router.Options {
|
||||
return r.opts
|
||||
}
|
||||
|
||||
func (r *registryRouter) Close() error {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return nil
|
||||
default:
|
||||
close(r.exit)
|
||||
r.rc.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryRouter) 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) {
|
||||
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 n, e := range r.eps {
|
||||
cep, ok := r.ceps[n]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ep := e.Endpoint
|
||||
var mMatch, hMatch, pMatch bool
|
||||
// 1. try method
|
||||
for _, m := range ep.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.Host) == 0 {
|
||||
hMatch = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hMatch {
|
||||
continue
|
||||
}
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("api host match %s", req.URL.Host)
|
||||
}
|
||||
|
||||
// 3. try path via google.api path matching
|
||||
for _, pathreg := range cep.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.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
|
||||
}
|
||||
}
|
||||
|
||||
if !pMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Percentage traffic
|
||||
// we got here, so its a match
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// no match
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
func (r *registryRouter) Route(req *http.Request) (*api.Service, error) {
|
||||
if r.isClosed() {
|
||||
return nil, errors.New("router closed")
|
||||
}
|
||||
|
||||
// try get an endpoint
|
||||
ep, err := r.Endpoint(req)
|
||||
if err == nil {
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
// error not nil
|
||||
// ignore that shit
|
||||
// TODO: don't ignore that shit
|
||||
|
||||
// get the service name
|
||||
rp, err := r.opts.Resolver.Resolve(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// service name
|
||||
name := rp.Name
|
||||
|
||||
// get service
|
||||
services, err := r.rc.GetService(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// only use endpoint matching when the meta handler is set aka api.Default
|
||||
switch r.opts.Handler {
|
||||
// rpc handlers
|
||||
case "meta", "api", "rpc":
|
||||
handler := r.opts.Handler
|
||||
|
||||
// set default handler to api
|
||||
if r.opts.Handler == "meta" {
|
||||
handler = "rpc"
|
||||
}
|
||||
|
||||
// construct api service
|
||||
return &api.Service{
|
||||
Name: name,
|
||||
Endpoint: &api.Endpoint{
|
||||
Name: rp.Method,
|
||||
Handler: handler,
|
||||
},
|
||||
Services: services,
|
||||
}, nil
|
||||
// http handler
|
||||
case "http", "proxy", "web":
|
||||
// construct api service
|
||||
return &api.Service{
|
||||
Name: name,
|
||||
Endpoint: &api.Endpoint{
|
||||
Name: req.URL.String(),
|
||||
Handler: r.opts.Handler,
|
||||
Host: []string{req.Host},
|
||||
Method: []string{req.Method},
|
||||
Path: []string{req.URL.Path},
|
||||
},
|
||||
Services: services,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown handler")
|
||||
}
|
||||
|
||||
func newRouter(opts ...router.Option) *registryRouter {
|
||||
options := router.NewOptions(opts...)
|
||||
r := ®istryRouter{
|
||||
exit: make(chan bool),
|
||||
opts: options,
|
||||
rc: cache.New(options.Registry),
|
||||
eps: make(map[string]*api.Service),
|
||||
ceps: make(map[string]*endpoint),
|
||||
}
|
||||
go r.watch()
|
||||
go r.refresh()
|
||||
return r
|
||||
}
|
||||
|
||||
// NewRouter returns the default router
|
||||
func NewRouter(opts ...router.Option) router.Router {
|
||||
return newRouter(opts...)
|
||||
}
|
34
api/router/registry/registry_test.go
Normal file
34
api/router/registry/registry_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStoreRegex(t *testing.T) {
|
||||
router := newRouter()
|
||||
router.store([]*registry.Service{
|
||||
{
|
||||
Name: "Foobar",
|
||||
Version: "latest",
|
||||
Endpoints: []*registry.Endpoint{
|
||||
{
|
||||
Name: "foo",
|
||||
Metadata: map[string]string{
|
||||
"endpoint": "FooEndpoint",
|
||||
"description": "Some description",
|
||||
"method": "POST",
|
||||
"path": "^/foo/$",
|
||||
"handler": "rpc",
|
||||
},
|
||||
},
|
||||
},
|
||||
Metadata: map[string]string{},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.Len(t, router.ceps["Foobar.foo"].pcreregs, 1)
|
||||
}
|
@@ -4,18 +4,13 @@ package router
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/unistack-org/micro/v3/api"
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
)
|
||||
|
||||
// DefaultRouter contains default router implementation
|
||||
var DefaultRouter Router
|
||||
|
||||
// Router is used to determine an endpoint for a request
|
||||
type Router interface {
|
||||
// Returns options
|
||||
Options() Options
|
||||
// Init initialize router
|
||||
Init(...Option) error
|
||||
// Stop the router
|
||||
Close() error
|
||||
// Endpoint returns an api.Service endpoint or an error if it does not exist
|
||||
@@ -26,6 +21,4 @@ type Router interface {
|
||||
Deregister(ep *api.Endpoint) error
|
||||
// Route returns an api.Service route
|
||||
Route(r *http.Request) (*api.Service, error)
|
||||
// String representation of router
|
||||
String() string
|
||||
}
|
||||
|
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
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package router
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile.go
|
||||
|
||||
@@ -33,11 +33,11 @@ type op struct {
|
||||
code OpCode
|
||||
|
||||
// str is a string operand of the code.
|
||||
// num is ignored if str is not empty.
|
||||
// operand is ignored if str is not empty.
|
||||
str string
|
||||
|
||||
// num is a numeric operand of the code.
|
||||
num int
|
||||
// operand is a numeric operand of the code.
|
||||
operand int
|
||||
}
|
||||
|
||||
func (w wildcard) compile() []op {
|
||||
@@ -62,13 +62,13 @@ func (l literal) compile() []op {
|
||||
}
|
||||
|
||||
func (v variable) compile() []op {
|
||||
ops := make([]op, 0, len(v.segments))
|
||||
var ops []op
|
||||
for _, s := range v.segments {
|
||||
ops = append(ops, s.compile()...)
|
||||
}
|
||||
ops = append(ops, op{
|
||||
code: OpConcatN,
|
||||
num: len(v.segments),
|
||||
code: OpConcatN,
|
||||
operand: len(v.segments),
|
||||
}, op{
|
||||
code: OpCapture,
|
||||
str: v.path,
|
||||
@@ -78,7 +78,7 @@ func (v variable) compile() []op {
|
||||
}
|
||||
|
||||
func (t template) Compile() Template {
|
||||
rawOps := make([]op, 0, len(t.segments))
|
||||
var rawOps []op
|
||||
for _, s := range t.segments {
|
||||
rawOps = append(rawOps, s.compile()...)
|
||||
}
|
||||
@@ -92,12 +92,8 @@ func (t template) Compile() Template {
|
||||
for _, op := range rawOps {
|
||||
ops = append(ops, int(op.code))
|
||||
if op.str == "" {
|
||||
ops = append(ops, op.num)
|
||||
ops = append(ops, op.operand)
|
||||
} else {
|
||||
// eof segment literal represents the "/" path pattern
|
||||
if op.str == eof {
|
||||
op.str = ""
|
||||
}
|
||||
if _, ok := consts[op.str]; !ok {
|
||||
consts[op.str] = len(pool)
|
||||
pool = append(pool, op.str)
|
@@ -1,4 +1,4 @@
|
||||
package router
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile_test.go
|
||||
|
||||
@@ -21,13 +21,6 @@ func TestCompile(t *testing.T) {
|
||||
fields []string
|
||||
}{
|
||||
{},
|
||||
{
|
||||
segs: []segment{
|
||||
literal(eof),
|
||||
},
|
||||
ops: []int{int(OpLitPush), 0},
|
||||
pool: []string{""},
|
||||
},
|
||||
{
|
||||
segs: []segment{
|
||||
wildcard{},
|
@@ -1,10 +1,12 @@
|
||||
package router
|
||||
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.
|
||||
@@ -47,7 +49,9 @@ func tokenize(path string) (tokens []string, verb string) {
|
||||
field
|
||||
nested
|
||||
)
|
||||
st := init
|
||||
var (
|
||||
st = init
|
||||
)
|
||||
for path != "" {
|
||||
var idx int
|
||||
switch st {
|
||||
@@ -80,30 +84,8 @@ func tokenize(path string) (tokens []string, verb string) {
|
||||
}
|
||||
|
||||
l := len(tokens)
|
||||
// See
|
||||
// https://github.com/grpc-ecosystem/grpc-gateway/pull/1947#issuecomment-774523693 ;
|
||||
// although normal and backwards-compat logic here is to use the last index
|
||||
// of a colon, if the final segment is a variable followed by a colon, the
|
||||
// part following the colon must be a verb. Hence if the previous token is
|
||||
// an end var marker, we switch the index we're looking for to Index instead
|
||||
// of LastIndex, so that we correctly grab the remaining part of the path as
|
||||
// the verb.
|
||||
var penultimateTokenIsEndVar bool
|
||||
switch l {
|
||||
case 0, 1:
|
||||
// Not enough to be variable so skip this logic and don't result in an
|
||||
// invalid index
|
||||
default:
|
||||
penultimateTokenIsEndVar = tokens[l-2] == "}"
|
||||
}
|
||||
t := tokens[l-1]
|
||||
var idx int
|
||||
if penultimateTokenIsEndVar {
|
||||
idx = strings.Index(t, ":")
|
||||
} else {
|
||||
idx = strings.LastIndex(t, ":")
|
||||
}
|
||||
if idx == 0 {
|
||||
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:]
|
||||
@@ -120,17 +102,22 @@ type parser struct {
|
||||
|
||||
// topLevelSegments is the target of this parser.
|
||||
func (p *parser) topLevelSegments() ([]segment, error) {
|
||||
if _, err := p.accept(typeEOF); err == nil {
|
||||
p.tokens = p.tokens[:0]
|
||||
return []segment{literal(eof)}, nil
|
||||
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
|
||||
}
|
||||
|
||||
@@ -140,6 +127,9 @@ func (p *parser) segments() ([]segment, error) {
|
||||
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 {
|
||||
@@ -150,6 +140,9 @@ func (p *parser) segments() ([]segment, error) {
|
||||
return segs, err
|
||||
}
|
||||
segs = append(segs, s)
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("accept segment: %q; %q", p.accepted, p.tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +275,6 @@ func (p *parser) accept(term termType) (string, error) {
|
||||
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||
// / "*" / "+" / "," / ";" / "="
|
||||
// pct-encoded = "%" HEXDIG HEXDIG
|
||||
//nolint:gocyclo
|
||||
func expectPChars(t string) error {
|
||||
const (
|
||||
init = iota
|
@@ -1,21 +1,20 @@
|
||||
package router
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/parse_test.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
func TestTokenize(t *testing.T) {
|
||||
for _, spec := range []struct {
|
||||
src string
|
||||
tokens []string
|
||||
verb string
|
||||
}{
|
||||
{
|
||||
src: "",
|
||||
@@ -84,74 +83,32 @@ func TestTokenize(t *testing.T) {
|
||||
eof,
|
||||
},
|
||||
},
|
||||
{
|
||||
src: "v1/a/{endpoint}:a",
|
||||
tokens: []string{
|
||||
"v1", "/",
|
||||
"a", "/",
|
||||
"{", "endpoint", "}",
|
||||
eof,
|
||||
},
|
||||
verb: "a",
|
||||
},
|
||||
{
|
||||
src: "v1/a/{endpoint}:b:c",
|
||||
tokens: []string{
|
||||
"v1", "/",
|
||||
"a", "/",
|
||||
"{", "endpoint", "}",
|
||||
eof,
|
||||
},
|
||||
verb: "b:c",
|
||||
},
|
||||
} {
|
||||
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)
|
||||
}
|
||||
|
||||
switch {
|
||||
case spec.verb != "":
|
||||
if got, want := verb, spec.verb; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("tokenize(%q) = %q, _; want %q, _", spec.src, got, want)
|
||||
}
|
||||
|
||||
default:
|
||||
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)
|
||||
}
|
||||
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{eof},
|
||||
want: []segment{
|
||||
literal(eof),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Note: this case will never arise as tokenize() will never return such a sequence of tokens
|
||||
// and even if it does it will be treated as [eof]
|
||||
tokens: []string{eof, "v1", eof},
|
||||
want: []segment{
|
||||
literal(eof),
|
||||
},
|
||||
},
|
||||
{
|
||||
tokens: []string{"v1", eof},
|
||||
want: []segment{
|
||||
@@ -251,8 +208,7 @@ func TestParseSegments(t *testing.T) {
|
||||
"a", "/", "b", "/", "*", "/", "c",
|
||||
"}", "/",
|
||||
"**",
|
||||
eof,
|
||||
},
|
||||
eof},
|
||||
want: []segment{
|
||||
literal("v1"),
|
||||
variable{
|
||||
@@ -293,6 +249,7 @@ func TestParseSegments(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseSegmentsWithErrors(t *testing.T) {
|
||||
flag.Set("v", "3")
|
||||
for _, spec := range []struct {
|
||||
tokens []string
|
||||
}{
|
||||
@@ -316,6 +273,10 @@ func TestParseSegmentsWithErrors(t *testing.T) {
|
||||
// invalid percent-encoding
|
||||
tokens: []string{"a%2z", eof},
|
||||
},
|
||||
{
|
||||
// empty segments
|
||||
tokens: []string{eof},
|
||||
},
|
||||
{
|
||||
// unterminated variable
|
||||
tokens: []string{"{", "name", eof},
|
||||
@@ -355,6 +316,6 @@ func TestParseSegmentsWithErrors(t *testing.T) {
|
||||
t.Errorf("parser{%q}.segments() succeeded; want InvalidTemplateError; accepted %#v", spec.tokens, segs)
|
||||
continue
|
||||
}
|
||||
logger.Info(context.TODO(), err.Error())
|
||||
logger.Info(err)
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package router
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/utilities/pattern.go
|
||||
|
@@ -1,14 +1,13 @@
|
||||
package router
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/runtime/pattern.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,12 +22,11 @@ type rop struct {
|
||||
operand int
|
||||
}
|
||||
|
||||
// Pattern is a template pattern of http request paths defined in
|
||||
// https://github.com/googleapis/googleapis/blob/master/google/api/http.proto
|
||||
// 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 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
|
||||
@@ -38,25 +36,42 @@ type Pattern struct {
|
||||
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) (Pattern, error) {
|
||||
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.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "unsupported version: %d", version)
|
||||
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.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "odd number of ops codes: %d", l)
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("odd number of ops codes: %d", l)
|
||||
}
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
@@ -80,8 +95,8 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er
|
||||
stack++
|
||||
case OpPushM:
|
||||
if pushMSeen {
|
||||
if logger.V(logger.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "pushM appears twice")
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debug("pushM appears twice")
|
||||
}
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
@@ -89,8 +104,8 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er
|
||||
stack++
|
||||
case OpLitPush:
|
||||
if op.operand < 0 || len(pool) <= op.operand {
|
||||
if logger.V(logger.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "negative literal index: %d", op.operand)
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("negative literal index: %d", op.operand)
|
||||
}
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
@@ -100,23 +115,23 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er
|
||||
stack++
|
||||
case OpConcatN:
|
||||
if op.operand <= 0 {
|
||||
if logger.V(logger.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "negative concat size: %d", op.operand)
|
||||
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.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "stack underflow")
|
||||
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.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "variable name index out of bound: %d", op.operand)
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("variable name index out of bound: %d", op.operand)
|
||||
}
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
@@ -125,14 +140,14 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er
|
||||
vars = append(vars, v)
|
||||
stack--
|
||||
if stack < 0 {
|
||||
if logger.V(logger.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "stack underflow")
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debug("stack underflow")
|
||||
}
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
default:
|
||||
if logger.V(logger.TraceLevel) {
|
||||
logger.Trace(context.TODO(), "invalid opcode: %d", op.code)
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("invalid opcode: %d", op.code)
|
||||
}
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
@@ -143,20 +158,21 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er
|
||||
typedOps = append(typedOps, op)
|
||||
}
|
||||
return Pattern{
|
||||
ops: typedOps,
|
||||
pool: pool,
|
||||
vars: vars,
|
||||
stacksize: maxstack,
|
||||
tailLen: tailLen,
|
||||
verb: verb,
|
||||
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.FatalLevel) {
|
||||
logger.Fatal(context.TODO(), "Pattern initialization failed: %v", err)
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Fatalf("Pattern initialization failed: %v", err)
|
||||
}
|
||||
}
|
||||
return p
|
||||
@@ -165,10 +181,9 @@ func MustPattern(p Pattern, err error) Pattern {
|
||||
// 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.
|
||||
//nolint:gocyclo
|
||||
func (p Pattern) Match(components []string, verb string) (map[string]string, error) {
|
||||
if p.verb != verb {
|
||||
if p.verb != "" {
|
||||
if p.assumeColonVerb || p.verb != "" {
|
||||
return nil, ErrNotMatch
|
||||
}
|
||||
if len(components) == 0 {
|
||||
@@ -177,6 +192,7 @@ func (p Pattern) Match(components []string, verb string) (map[string]string, err
|
||||
components = append([]string{}, components...)
|
||||
components[len(components)-1] += ":" + verb
|
||||
}
|
||||
verb = ""
|
||||
}
|
||||
|
||||
var pos int
|
||||
@@ -220,7 +236,7 @@ func (p Pattern) Match(components []string, verb string) (map[string]string, err
|
||||
if pos < l {
|
||||
return nil, ErrNotMatch
|
||||
}
|
||||
bindings := make(map[string]string, len(captured))
|
||||
bindings := make(map[string]string)
|
||||
for i, val := range captured {
|
||||
bindings[p.vars[i]] = val
|
||||
}
|
||||
@@ -257,3 +273,11 @@ func (p Pattern) String() string {
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package router
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/types.go
|
||||
|
||||
@@ -42,7 +42,7 @@ func (l literal) String() string {
|
||||
}
|
||||
|
||||
func (v variable) String() string {
|
||||
segs := make([]string, 0, len(v.segments))
|
||||
var segs []string
|
||||
for _, s := range v.segments {
|
||||
segs = append(segs, s.String())
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (v variable) String() string {
|
||||
}
|
||||
|
||||
func (t template) String() string {
|
||||
segs := make([]string, 0, len(t.segments))
|
||||
var segs []string
|
||||
for _, s := range t.segments {
|
||||
segs = append(segs, s.String())
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package router
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/types_test.go
|
||||
|
28
api/server/acme/acme.go
Normal file
28
api/server/acme/acme.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Package acme abstracts away various ACME libraries
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrProviderNotImplemented can be returned when attempting to
|
||||
// instantiate an unimplemented provider
|
||||
ErrProviderNotImplemented = errors.New("Provider not implemented")
|
||||
)
|
||||
|
||||
// Provider is a ACME provider interface
|
||||
type Provider interface {
|
||||
// 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
|
||||
const (
|
||||
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
)
|
46
api/server/acme/autocert/autocert.go
Normal file
46
api/server/acme/autocert/autocert.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Package autocert is the ACME provider from golang.org/x/crypto/acme/autocert
|
||||
// This provider does not take any config.
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/server/acme"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// autoCertACME is the ACME provider from golang.org/x/crypto/acme/autocert
|
||||
type autocertProvider struct{}
|
||||
|
||||
// Listen implements acme.Provider
|
||||
func (a *autocertProvider) Listen(hosts ...string) (net.Listener, error) {
|
||||
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
|
||||
func NewProvider() acme.Provider {
|
||||
return &autocertProvider{}
|
||||
}
|
16
api/server/acme/autocert/autocert_test.go
Normal file
16
api/server/acme/autocert/autocert_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAutocert(t *testing.T) {
|
||||
l := NewProvider()
|
||||
if _, ok := l.(*autocertProvider); !ok {
|
||||
t.Error("NewProvider() didn't return an autocertProvider")
|
||||
}
|
||||
// TODO: Travis CI doesn't let us bind :443
|
||||
// if _, err := l.NewListener(); err != nil {
|
||||
// t.Error(err.Error())
|
||||
// }
|
||||
}
|
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)
|
||||
}
|
68
api/server/acme/certmagic/certmagic.go
Normal file
68
api/server/acme/certmagic/certmagic.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Package certmagic is the ACME provider from github.com/caddyserver/certmagic
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/micro/go-micro/v2/api/server/acme"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
type certmagicProvider struct {
|
||||
opts acme.Options
|
||||
}
|
||||
|
||||
// TODO: set self-contained options
|
||||
func (c *certmagicProvider) setup() {
|
||||
certmagic.DefaultACME.CA = c.opts.CA
|
||||
if c.opts.ChallengeProvider != nil {
|
||||
// Enabling DNS Challenge disables the other challenges
|
||||
certmagic.DefaultACME.DNSProvider = c.opts.ChallengeProvider
|
||||
}
|
||||
if c.opts.OnDemand {
|
||||
certmagic.Default.OnDemand = new(certmagic.OnDemandConfig)
|
||||
}
|
||||
if c.opts.Cache != nil {
|
||||
// already validated by new()
|
||||
certmagic.Default.Storage = c.opts.Cache.(certmagic.Storage)
|
||||
}
|
||||
// If multiple instances of the provider are running, inject some
|
||||
// randomness so they don't collide
|
||||
// RenewalWindowRatio [0.33 - 0.50)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randomRatio := float64(rand.Intn(17)+33) * 0.01
|
||||
certmagic.Default.RenewalWindowRatio = randomRatio
|
||||
}
|
||||
|
||||
func (c *certmagicProvider) Listen(hosts ...string) (net.Listener, error) {
|
||||
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()
|
||||
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
if opts.Cache != nil {
|
||||
if _, ok := opts.Cache.(certmagic.Storage); !ok {
|
||||
logger.Fatal("ACME: cache provided doesn't implement certmagic's Storage interface")
|
||||
}
|
||||
}
|
||||
|
||||
return &certmagicProvider{
|
||||
opts: opts,
|
||||
}
|
||||
}
|
147
api/server/acme/certmagic/storage.go
Normal file
147
api/server/acme/certmagic/storage.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/micro/go-micro/v2/store"
|
||||
"github.com/micro/go-micro/v2/sync"
|
||||
)
|
||||
|
||||
// File represents a "File" that will be stored in store.Store - the contents and last modified time
|
||||
type File struct {
|
||||
// last modified time
|
||||
LastModified time.Time
|
||||
// Contents
|
||||
Contents []byte
|
||||
}
|
||||
|
||||
// storage is an implementation of certmagic.Storage using micro's sync.Map and store.Store interfaces.
|
||||
// As certmagic storage expects a filesystem (with stat() abilities) we have to implement
|
||||
// the bare minimum of metadata.
|
||||
type storage struct {
|
||||
lock sync.Sync
|
||||
store store.Store
|
||||
}
|
||||
|
||||
func (s *storage) Lock(key string) error {
|
||||
return s.lock.Lock(key, sync.LockTTL(10*time.Minute))
|
||||
}
|
||||
|
||||
func (s *storage) Unlock(key string) error {
|
||||
return s.lock.Unlock(key)
|
||||
}
|
||||
|
||||
func (s *storage) Store(key string, value []byte) error {
|
||||
f := File{
|
||||
LastModified: time.Now(),
|
||||
Contents: value,
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
e := gob.NewEncoder(buf)
|
||||
if err := e.Encode(f); err != nil {
|
||||
return err
|
||||
}
|
||||
r := &store.Record{
|
||||
Key: key,
|
||||
Value: buf.Bytes(),
|
||||
}
|
||||
return s.store.Write(r)
|
||||
}
|
||||
|
||||
func (s *storage) Load(key string) ([]byte, error) {
|
||||
if !s.Exists(key) {
|
||||
return nil, certmagic.ErrNotExist(errors.New(key + " doesn't exist"))
|
||||
}
|
||||
records, err := s.store.Read(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(records) != 1 {
|
||||
return nil, fmt.Errorf("ACME Storage: multiple records matched key %s", key)
|
||||
}
|
||||
b := bytes.NewBuffer(records[0].Value)
|
||||
d := gob.NewDecoder(b)
|
||||
var f File
|
||||
err = d.Decode(&f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Contents, nil
|
||||
}
|
||||
|
||||
func (s *storage) Delete(key string) error {
|
||||
return s.store.Delete(key)
|
||||
}
|
||||
|
||||
func (s *storage) Exists(key string) bool {
|
||||
if _, err := s.store.Read(key); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *storage) List(prefix string, recursive bool) ([]string, error) {
|
||||
keys, err := s.store.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//nolint:prealloc
|
||||
var results []string
|
||||
for _, k := range keys {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
results = append(results, k)
|
||||
}
|
||||
}
|
||||
if recursive {
|
||||
return results, nil
|
||||
}
|
||||
keysMap := make(map[string]bool)
|
||||
for _, key := range results {
|
||||
dir := strings.Split(strings.TrimPrefix(key, prefix+"/"), "/")
|
||||
keysMap[dir[0]] = true
|
||||
}
|
||||
results = make([]string, 0)
|
||||
for k := range keysMap {
|
||||
results = append(results, path.Join(prefix, k))
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *storage) Stat(key string) (certmagic.KeyInfo, error) {
|
||||
records, err := s.store.Read(key)
|
||||
if err != nil {
|
||||
return certmagic.KeyInfo{}, err
|
||||
}
|
||||
if len(records) != 1 {
|
||||
return certmagic.KeyInfo{}, fmt.Errorf("ACME Storage: multiple records matched key %s", key)
|
||||
}
|
||||
b := bytes.NewBuffer(records[0].Value)
|
||||
d := gob.NewDecoder(b)
|
||||
var f File
|
||||
err = d.Decode(&f)
|
||||
if err != nil {
|
||||
return certmagic.KeyInfo{}, err
|
||||
}
|
||||
return certmagic.KeyInfo{
|
||||
Key: key,
|
||||
Modified: f.LastModified,
|
||||
Size: int64(len(f.Contents)),
|
||||
IsTerminal: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewStorage returns a certmagic.Storage backed by a go-micro/lock and go-micro/store
|
||||
func NewStorage(lock sync.Sync, store store.Store) certmagic.Storage {
|
||||
return &storage{
|
||||
lock: lock,
|
||||
store: store,
|
||||
}
|
||||
}
|
73
api/server/acme/options.go
Normal file
73
api/server/acme/options.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package acme
|
||||
|
||||
import "github.com/go-acme/lego/v3/challenge"
|
||||
|
||||
// Option (or Options) are passed to New() to configure providers
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options represents various options you can present to ACME providers
|
||||
type Options struct {
|
||||
// AcceptTLS must be set to true to indicate that you have read your
|
||||
// provider's terms of service.
|
||||
AcceptToS bool
|
||||
// CA is the CA to use
|
||||
CA string
|
||||
// ChallengeProvider is a go-acme/lego challenge provider. Set this if you
|
||||
// want to use DNS Challenges. Otherwise, tls-alpn-01 will be used
|
||||
ChallengeProvider challenge.Provider
|
||||
// Issue certificates for domains on demand. Otherwise, certs will be
|
||||
// retrieved / issued on start-up.
|
||||
OnDemand bool
|
||||
// Cache is a storage interface. Most ACME libraries have an cache, but
|
||||
// there's no defined interface, so if you consume this option
|
||||
// sanity check it before using.
|
||||
Cache interface{}
|
||||
}
|
||||
|
||||
// AcceptToS indicates whether you accept your CA's terms of service
|
||||
func AcceptToS(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.AcceptToS = b
|
||||
}
|
||||
}
|
||||
|
||||
// CA sets the CA of an acme.Options
|
||||
func CA(CA string) Option {
|
||||
return func(o *Options) {
|
||||
o.CA = CA
|
||||
}
|
||||
}
|
||||
|
||||
// ChallengeProvider sets the Challenge provider of an acme.Options
|
||||
// if set, it enables the DNS challenge, otherwise tls-alpn-01 will be used.
|
||||
func ChallengeProvider(p challenge.Provider) Option {
|
||||
return func(o *Options) {
|
||||
o.ChallengeProvider = p
|
||||
}
|
||||
}
|
||||
|
||||
// OnDemand enables on-demand certificate issuance. Not recommended for use
|
||||
// with the DNS challenge, as the first connection may be very slow.
|
||||
func OnDemand(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.OnDemand = b
|
||||
}
|
||||
}
|
||||
|
||||
// Cache provides a cache / storage interface to the underlying ACME library
|
||||
// as there is no standard, this needs to be validated by the underlying
|
||||
// implentation.
|
||||
func Cache(c interface{}) Option {
|
||||
return func(o *Options) {
|
||||
o.Cache = c
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultOptions uses the Let's Encrypt Production CA, with DNS Challenge disabled.
|
||||
func DefaultOptions() Options {
|
||||
return Options{
|
||||
AcceptToS: true,
|
||||
CA: LetsEncryptProductionCA,
|
||||
OnDemand: true,
|
||||
}
|
||||
}
|
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")
|
||||
}
|
120
api/server/http/http.go
Normal file
120
api/server/http/http.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Package http provides a http server with features; acme, cors, etc
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/micro/go-micro/v2/api/server"
|
||||
"github.com/micro/go-micro/v2/api/server/cors"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
mux *http.ServeMux
|
||||
opts server.Options
|
||||
|
||||
mtx sync.RWMutex
|
||||
address string
|
||||
exit chan chan error
|
||||
}
|
||||
|
||||
func NewServer(address string, opts ...server.Option) server.Server {
|
||||
var options server.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &httpServer{
|
||||
opts: options,
|
||||
mux: http.NewServeMux(),
|
||||
address: address,
|
||||
exit: make(chan chan error),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *httpServer) Address() string {
|
||||
s.mtx.RLock()
|
||||
defer s.mtx.RUnlock()
|
||||
return s.address
|
||||
}
|
||||
|
||||
func (s *httpServer) Init(opts ...server.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&s.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *httpServer) Handle(path string, handler http.Handler) {
|
||||
// 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 {
|
||||
var l net.Listener
|
||||
var err error
|
||||
|
||||
if s.opts.EnableACME && s.opts.ACMEProvider != nil {
|
||||
// should we check the address to make sure its using :443?
|
||||
l, err = s.opts.ACMEProvider.Listen(s.opts.ACMEHosts...)
|
||||
} else if s.opts.EnableTLS && s.opts.TLSConfig != nil {
|
||||
l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig)
|
||||
} else {
|
||||
// otherwise plain listen
|
||||
l, err = net.Listen("tcp", s.address)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if logger.V(logger.InfoLevel, logger.DefaultLogger) {
|
||||
logger.Infof("HTTP API Listening on %s", l.Addr().String())
|
||||
}
|
||||
|
||||
s.mtx.Lock()
|
||||
s.address = l.Addr().String()
|
||||
s.mtx.Unlock()
|
||||
|
||||
go func() {
|
||||
if err := http.Serve(l, s.mux); err != nil {
|
||||
// temporary fix
|
||||
//logger.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ch := <-s.exit
|
||||
ch <- l.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *httpServer) Stop() error {
|
||||
ch := make(chan error)
|
||||
s.exit <- ch
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (s *httpServer) String() string {
|
||||
return "http"
|
||||
}
|
41
api/server/http/http_test.go
Normal file
41
api/server/http/http_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPServer(t *testing.T) {
|
||||
testResponse := "hello world"
|
||||
|
||||
s := NewServer("localhost:0")
|
||||
|
||||
s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, testResponse)
|
||||
}))
|
||||
|
||||
if err := s.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rsp, err := http.Get(fmt.Sprintf("http://%s/", s.Address()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != testResponse {
|
||||
t.Fatalf("Unexpected response, got %s, expected %s", string(b), testResponse)
|
||||
}
|
||||
|
||||
if err := s.Stop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
72
api/server/options.go
Normal file
72
api/server/options.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/server/acme"
|
||||
)
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
type Options struct {
|
||||
EnableACME bool
|
||||
EnableCORS bool
|
||||
ACMEProvider acme.Provider
|
||||
EnableTLS bool
|
||||
ACMEHosts []string
|
||||
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 {
|
||||
return func(o *Options) {
|
||||
o.EnableACME = b
|
||||
}
|
||||
}
|
||||
|
||||
func ACMEHosts(hosts ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.ACMEHosts = hosts
|
||||
}
|
||||
}
|
||||
|
||||
func ACMEProvider(p acme.Provider) Option {
|
||||
return func(o *Options) {
|
||||
o.ACMEProvider = p
|
||||
}
|
||||
}
|
||||
|
||||
func EnableTLS(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.EnableTLS = b
|
||||
}
|
||||
}
|
||||
|
||||
func TLSConfig(t *tls.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSConfig = t
|
||||
}
|
||||
}
|
||||
|
||||
func Resolver(r resolver.Resolver) Option {
|
||||
return func(o *Options) {
|
||||
o.Resolver = r
|
||||
}
|
||||
}
|
15
api/server/server.go
Normal file
15
api/server/server.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Package server provides an API gateway server which handles inbound requests
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Server serves api requests
|
||||
type Server interface {
|
||||
Address() string
|
||||
Init(opts ...Option) error
|
||||
Handle(path string, handler http.Handler)
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
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 {}
|
44
auth/auth.go
44
auth/auth.go
@@ -5,8 +5,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/unistack-org/micro/v3/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,8 +17,6 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultAuth holds default auth implementation
|
||||
DefaultAuth Auth = NewAuth()
|
||||
// ErrInvalidToken is when the token provided is not valid
|
||||
ErrInvalidToken = errors.New("invalid token provided")
|
||||
// ErrForbidden is when a user does not have the necessary scope to access a resource
|
||||
@@ -30,7 +26,7 @@ var (
|
||||
// Auth provides authentication and authorization
|
||||
type Auth interface {
|
||||
// Init the auth
|
||||
Init(opts ...Option) error
|
||||
Init(opts ...Option)
|
||||
// Options set for auth
|
||||
Options() Options
|
||||
// Generate a new account
|
||||
@@ -53,30 +49,30 @@ type Auth interface {
|
||||
|
||||
// Account provided by an auth provider
|
||||
type Account struct {
|
||||
// Metadata any other associated metadata
|
||||
Metadata metadata.Metadata `json:"metadata"`
|
||||
// ID of the account e.g. email or uuid
|
||||
// ID of the account e.g. email
|
||||
ID string `json:"id"`
|
||||
// Type of the account, e.g. service
|
||||
Type string `json:"type"`
|
||||
// Issuer of the account
|
||||
Issuer string `json:"issuer"`
|
||||
// Secret for the account, e.g. the password
|
||||
Secret string `json:"secret"`
|
||||
// Any other associated metadata
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
// Scopes the account has access to
|
||||
Scopes []string `json:"scopes"`
|
||||
// Secret for the account, e.g. the password
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// Token can be short or long lived
|
||||
type Token struct {
|
||||
// Time of token creation
|
||||
Created time.Time `json:"created"`
|
||||
// Time of token expiry
|
||||
Expiry time.Time `json:"expiry"`
|
||||
// The token to be used for accessing resources
|
||||
AccessToken string `json:"access_token"`
|
||||
// RefreshToken to be used to generate a new token
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
// Time of token creation
|
||||
Created time.Time `json:"created"`
|
||||
// Time of token expiry
|
||||
Expiry time.Time `json:"expiry"`
|
||||
}
|
||||
|
||||
// Expired returns a boolean indicating if the token needs to be refreshed
|
||||
@@ -106,15 +102,17 @@ const (
|
||||
|
||||
// Rule is used to verify access to a resource
|
||||
type Rule struct {
|
||||
// Resource that rule belongs to
|
||||
Resource *Resource
|
||||
// ID of the rule
|
||||
// ID of the rule, e.g. "public"
|
||||
ID string
|
||||
// Scope of the rule
|
||||
// Scope the rule requires, a blank scope indicates open to the public and * indicates the rule
|
||||
// applies to any valid account
|
||||
Scope string
|
||||
// Access flag allow/deny
|
||||
// Resource the rule applies to
|
||||
Resource *Resource
|
||||
// Access determines if the rule grants or denies access to the resource
|
||||
Access Access
|
||||
// Priority holds the rule priority
|
||||
// Priority the rule should take when verifying a request, the higher the value the sooner the
|
||||
// rule will be applied
|
||||
Priority int32
|
||||
}
|
||||
|
||||
@@ -125,17 +123,11 @@ type accountKey struct{}
|
||||
// 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) {
|
||||
if ctx == nil {
|
||||
return nil, false
|
||||
}
|
||||
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 {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return context.WithValue(ctx, accountKey{}, account)
|
||||
}
|
||||
|
88
auth/default.go
Normal file
88
auth/default.go
Normal file
@@ -0,0 +1,88 @@
|
||||
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,
|
||||
Secret: options.Secret,
|
||||
Metadata: options.Metadata,
|
||||
Scopes: options.Scopes,
|
||||
Issuer: n.Options().Namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Grant access to a resource
|
||||
func (n *noop) Grant(rule *Rule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Revoke access to a resource
|
||||
func (n *noop) Revoke(rule *Rule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules used to verify requests
|
||||
func (n *noop) Rules(opts ...RulesOption) ([]*Rule, error) {
|
||||
return []*Rule{}, nil
|
||||
}
|
||||
|
||||
// Verify an account has access to a resource
|
||||
func (n *noop) Verify(acc *Account, res *Resource, opts ...VerifyOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inspect a token
|
||||
func (n *noop) Inspect(token string) (*Account, error) {
|
||||
return &Account{ID: uuid.New().String(), Issuer: n.Options().Namespace}, nil
|
||||
}
|
||||
|
||||
// Token generation using an account id and secret
|
||||
func (n *noop) Token(opts ...TokenOption) (*Token, error) {
|
||||
return &Token{}, nil
|
||||
}
|
147
auth/jwt/jwt.go
Normal file
147
auth/jwt/jwt.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/v2/auth"
|
||||
"github.com/micro/go-micro/v2/auth/rules"
|
||||
"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 jwt struct {
|
||||
options auth.Options
|
||||
jwt token.Provider
|
||||
rules []*auth.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)
|
||||
}
|
||||
|
||||
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,
|
||||
Scopes: options.Scopes,
|
||||
Metadata: options.Metadata,
|
||||
Issuer: j.Options().Namespace,
|
||||
}
|
||||
|
||||
// generate a JWT secret which can be provided to the Token() method
|
||||
// and exchanged for an access token
|
||||
secret, err := j.jwt.Generate(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.Secret = secret.Token
|
||||
|
||||
// return the account
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (j *jwt) Grant(rule *auth.Rule) error {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
j.rules = append(j.rules, rule)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *jwt) Revoke(rule *auth.Rule) error {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
rules := []*auth.Rule{}
|
||||
for _, r := range j.rules {
|
||||
if r.ID != rule.ID {
|
||||
rules = append(rules, r)
|
||||
}
|
||||
}
|
||||
|
||||
j.rules = rules
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *jwt) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyOption) error {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
var options auth.VerifyOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return rules.Verify(j.rules, acc, res)
|
||||
}
|
||||
|
||||
func (j *jwt) Rules(opts ...auth.RulesOption) ([]*auth.Rule, error) {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
return j.rules, nil
|
||||
}
|
||||
|
||||
func (j *jwt) Inspect(token string) (*auth.Account, error) {
|
||||
return j.jwt.Inspect(token)
|
||||
}
|
||||
|
||||
func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) {
|
||||
options := auth.NewTokenOptions(opts...)
|
||||
|
||||
secret := options.RefreshToken
|
||||
if len(options.Secret) > 0 {
|
||||
secret = options.Secret
|
||||
}
|
||||
|
||||
account, err := j.jwt.Inspect(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
access, err := j.jwt.Generate(account, 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
|
||||
}
|
79
auth/noop.go
79
auth/noop.go
@@ -1,79 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type noopAuth struct {
|
||||
opts Options
|
||||
}
|
||||
|
||||
// String returns the name of the implementation
|
||||
func (n *noopAuth) String() string {
|
||||
return "noop"
|
||||
}
|
||||
|
||||
// Init the auth
|
||||
func (n *noopAuth) Init(opts ...Option) error {
|
||||
for _, o := range opts {
|
||||
o(&n.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options set for auth
|
||||
func (n *noopAuth) Options() Options {
|
||||
return n.opts
|
||||
}
|
||||
|
||||
// Generate a new account
|
||||
func (n *noopAuth) Generate(id string, opts ...GenerateOption) (*Account, error) {
|
||||
options := NewGenerateOptions(opts...)
|
||||
|
||||
return &Account{
|
||||
ID: id,
|
||||
Secret: options.Secret,
|
||||
Metadata: options.Metadata,
|
||||
Scopes: options.Scopes,
|
||||
Issuer: n.Options().Issuer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Grant access to a resource
|
||||
func (n *noopAuth) Grant(rule *Rule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Revoke access to a resource
|
||||
func (n *noopAuth) Revoke(rule *Rule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules used to verify requests
|
||||
func (n *noopAuth) Rules(opts ...RulesOption) ([]*Rule, error) {
|
||||
return []*Rule{}, nil
|
||||
}
|
||||
|
||||
// Verify an account has access to a resource
|
||||
func (n *noopAuth) Verify(acc *Account, res *Resource, opts ...VerifyOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inspect a token
|
||||
func (n *noopAuth) Inspect(token string) (*Account, error) {
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Account{ID: uid.String(), Issuer: n.Options().Issuer}, nil
|
||||
}
|
||||
|
||||
// Token generation using an account id and secret
|
||||
func (n *noopAuth) Token(opts ...TokenOption) (*Token, error) {
|
||||
return &Token{}, nil
|
||||
}
|
||||
|
||||
// NewAuth returns new noop auth
|
||||
func NewAuth(opts ...Option) Auth {
|
||||
return &noopAuth{opts: NewOptions(opts...)}
|
||||
}
|
192
auth/options.go
192
auth/options.go
@@ -4,59 +4,48 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"github.com/unistack-org/micro/v3/metadata"
|
||||
"github.com/unistack-org/micro/v3/meter"
|
||||
"github.com/unistack-org/micro/v3/store"
|
||||
"github.com/unistack-org/micro/v3/tracer"
|
||||
"github.com/micro/go-micro/v2/auth/provider"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
"github.com/micro/go-micro/v2/store"
|
||||
)
|
||||
|
||||
// NewOptions creates Options struct from slice of options
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Tracer: tracer.DefaultTracer,
|
||||
Logger: logger.DefaultLogger,
|
||||
Meter: meter.DefaultMeter,
|
||||
}
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
if options.Client == nil {
|
||||
options.Client = client.DefaultClient
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Options struct holds auth options
|
||||
type Options struct {
|
||||
// Context holds the external options
|
||||
Context context.Context
|
||||
// Meter used for metrics
|
||||
Meter meter.Meter
|
||||
// Logger used for logging
|
||||
Logger logger.Logger
|
||||
// Tracer used for tracing
|
||||
Tracer tracer.Tracer
|
||||
// Store used for stre data
|
||||
Store store.Store
|
||||
// Token is the services token used to authenticate itself
|
||||
Token *Token
|
||||
// LoginURL is the relative url path where a user can login
|
||||
LoginURL string
|
||||
// PrivateKey for encoding JWTs
|
||||
PrivateKey string
|
||||
// PublicKey for decoding JWTs
|
||||
PublicKey string
|
||||
// Secret is used to authenticate the service
|
||||
Secret string
|
||||
// Namespace the service belongs to
|
||||
Namespace string
|
||||
// ID is the services auth ID
|
||||
ID string
|
||||
// Issuer of the service's account
|
||||
Issuer string
|
||||
// Name holds the auth name
|
||||
Name 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
|
||||
}
|
||||
|
||||
// Option func
|
||||
type Option func(o *Options)
|
||||
|
||||
// Addrs is the auth addresses to use
|
||||
@@ -66,17 +55,10 @@ func Addrs(addrs ...string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// Name sets the name
|
||||
func Name(n string) Option {
|
||||
// Namespace the service belongs to
|
||||
func Namespace(n string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = n
|
||||
}
|
||||
}
|
||||
|
||||
// Issuer of the services account
|
||||
func Issuer(i string) Option {
|
||||
return func(o *Options) {
|
||||
o.Issuer = i
|
||||
o.Namespace = n
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +98,13 @@ func ClientToken(token *Token) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -123,17 +112,26 @@ func LoginURL(url string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateOptions struct
|
||||
type GenerateOptions struct {
|
||||
Metadata metadata.Metadata
|
||||
Provider string
|
||||
Type string
|
||||
Secret string
|
||||
Issuer string
|
||||
Scopes []string
|
||||
// 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
|
||||
// Scopes the account has access too
|
||||
Scopes []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
|
||||
}
|
||||
|
||||
// GenerateOption func
|
||||
type GenerateOption func(o *GenerateOptions)
|
||||
|
||||
// WithSecret for the generated account
|
||||
@@ -151,9 +149,9 @@ func WithType(t string) GenerateOption {
|
||||
}
|
||||
|
||||
// WithMetadata for the generated account
|
||||
func WithMetadata(md metadata.Metadata) GenerateOption {
|
||||
func WithMetadata(md map[string]string) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Metadata = metadata.Copy(md)
|
||||
o.Metadata = md
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,13 +169,6 @@ func WithScopes(s ...string) GenerateOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithIssuer for the generated account
|
||||
func WithIssuer(i string) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Issuer = i
|
||||
}
|
||||
}
|
||||
|
||||
// NewGenerateOptions from a slice of options
|
||||
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
|
||||
var options GenerateOptions
|
||||
@@ -187,16 +178,17 @@ func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
|
||||
return options
|
||||
}
|
||||
|
||||
// TokenOptions struct
|
||||
type TokenOptions struct {
|
||||
ID string
|
||||
Secret string
|
||||
// ID for the account
|
||||
ID string
|
||||
// Secret for the account
|
||||
Secret string
|
||||
// RefreshToken is used to refesh a token
|
||||
RefreshToken string
|
||||
Issuer string
|
||||
Expiry time.Duration
|
||||
// Expiry is the time the token should live for
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
// TokenOption func
|
||||
type TokenOption func(o *TokenOptions)
|
||||
|
||||
// WithExpiry for the token
|
||||
@@ -206,7 +198,6 @@ func WithExpiry(ex time.Duration) TokenOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithCredentials sets tye id and secret
|
||||
func WithCredentials(id, secret string) TokenOption {
|
||||
return func(o *TokenOptions) {
|
||||
o.ID = id
|
||||
@@ -214,20 +205,12 @@ func WithCredentials(id, secret string) TokenOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithToken sets the refresh token
|
||||
func WithToken(rt string) TokenOption {
|
||||
return func(o *TokenOptions) {
|
||||
o.RefreshToken = rt
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenIssuer sets the token issuer option
|
||||
func WithTokenIssuer(iss string) TokenOption {
|
||||
return func(o *TokenOptions) {
|
||||
o.Issuer = iss
|
||||
}
|
||||
}
|
||||
|
||||
// NewTokenOptions from a slice of options
|
||||
func NewTokenOptions(opts ...TokenOption) TokenOptions {
|
||||
var options TokenOptions
|
||||
@@ -235,7 +218,7 @@ func NewTokenOptions(opts ...TokenOption) TokenOptions {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// set default expiry of token
|
||||
// set defualt expiry of token
|
||||
if options.Expiry == 0 {
|
||||
options.Expiry = time.Minute
|
||||
}
|
||||
@@ -243,69 +226,26 @@ func NewTokenOptions(opts ...TokenOption) TokenOptions {
|
||||
return options
|
||||
}
|
||||
|
||||
// VerifyOptions struct
|
||||
type VerifyOptions struct {
|
||||
Context context.Context
|
||||
Namespace string
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
// VerifyOption func
|
||||
type VerifyOption func(o *VerifyOptions)
|
||||
|
||||
// VerifyContext pass context to verify
|
||||
func VerifyContext(ctx context.Context) VerifyOption {
|
||||
return func(o *VerifyOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyNamespace sets thhe namespace for verify
|
||||
func VerifyNamespace(ns string) VerifyOption {
|
||||
return func(o *VerifyOptions) {
|
||||
o.Namespace = ns
|
||||
}
|
||||
}
|
||||
|
||||
// RulesOptions struct
|
||||
type RulesOptions struct {
|
||||
Context context.Context
|
||||
Namespace string
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
// RulesOption func
|
||||
type RulesOption func(o *RulesOptions)
|
||||
|
||||
// RulesContext pass rules context
|
||||
func RulesContext(ctx context.Context) RulesOption {
|
||||
return func(o *RulesOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// RulesNamespace sets the rule namespace
|
||||
func RulesNamespace(ns string) RulesOption {
|
||||
return func(o *RulesOptions) {
|
||||
o.Namespace = ns
|
||||
}
|
||||
}
|
||||
|
||||
// Logger sets the logger
|
||||
func Logger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// Meter sets the meter
|
||||
func Meter(m meter.Meter) Option {
|
||||
return func(o *Options) {
|
||||
o.Meter = m
|
||||
}
|
||||
}
|
||||
|
||||
// Tracer sets the meter
|
||||
func Tracer(t tracer.Tracer) Option {
|
||||
return func(o *Options) {
|
||||
o.Tracer = t
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|
@@ -1,16 +1,17 @@
|
||||
package auth
|
||||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/auth"
|
||||
)
|
||||
|
||||
// VerifyAccess an account has access to a resource using the rules provided. If the account does not have
|
||||
// Verify an account has access to a resource using the rules provided. If the account does not have
|
||||
// access an error will be returned. If there are no rules provided which match the resource, an error
|
||||
// will be returned
|
||||
//nolint:gocyclo
|
||||
func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
|
||||
func Verify(rules []*auth.Rule, acc *auth.Account, res *auth.Resource) error {
|
||||
// the rule is only to be applied if the type matches the resource or is catch-all (*)
|
||||
validTypes := []string{"*", res.Type}
|
||||
|
||||
@@ -28,7 +29,7 @@ func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
|
||||
}
|
||||
|
||||
// filter the rules to the ones which match the criteria above
|
||||
filteredRules := make([]*Rule, 0)
|
||||
filteredRules := make([]*auth.Rule, 0)
|
||||
for _, rule := range rules {
|
||||
if !include(validTypes, rule.Resource.Type) {
|
||||
continue
|
||||
@@ -50,9 +51,9 @@ func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
|
||||
// loop through the rules and check for a rule which applies to this account
|
||||
for _, rule := range filteredRules {
|
||||
// a blank scope indicates the rule applies to everyone, even nil accounts
|
||||
if rule.Scope == ScopePublic && rule.Access == AccessDenied {
|
||||
return ErrForbidden
|
||||
} else if rule.Scope == ScopePublic && rule.Access == AccessGranted {
|
||||
if rule.Scope == auth.ScopePublic && rule.Access == auth.AccessDenied {
|
||||
return auth.ErrForbidden
|
||||
} else if rule.Scope == auth.ScopePublic && rule.Access == auth.AccessGranted {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -62,29 +63,29 @@ func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
|
||||
}
|
||||
|
||||
// this rule applies to any account
|
||||
if rule.Scope == ScopeAccount && rule.Access == AccessDenied {
|
||||
return ErrForbidden
|
||||
} else if rule.Scope == ScopeAccount && rule.Access == AccessGranted {
|
||||
if rule.Scope == auth.ScopeAccount && rule.Access == auth.AccessDenied {
|
||||
return auth.ErrForbidden
|
||||
} else if rule.Scope == auth.ScopeAccount && rule.Access == auth.AccessGranted {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the account has the necessary scope
|
||||
if include(acc.Scopes, rule.Scope) && rule.Access == AccessDenied {
|
||||
return ErrForbidden
|
||||
} else if include(acc.Scopes, rule.Scope) && rule.Access == AccessGranted {
|
||||
if include(acc.Scopes, rule.Scope) && rule.Access == auth.AccessDenied {
|
||||
return auth.ErrForbidden
|
||||
} else if include(acc.Scopes, rule.Scope) && rule.Access == auth.AccessGranted {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if no rules matched then return forbidden
|
||||
return ErrForbidden
|
||||
return auth.ErrForbidden
|
||||
}
|
||||
|
||||
// include is a helper function which checks to see if the slice contains the value. includes is
|
||||
// not case sensitive.
|
||||
func include(slice []string, val string) bool {
|
||||
for _, s := range slice {
|
||||
if strings.EqualFold(s, val) {
|
||||
if strings.ToLower(s) == strings.ToLower(val) {
|
||||
return true
|
||||
}
|
||||
}
|
@@ -1,48 +1,50 @@
|
||||
package auth
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/v2/auth"
|
||||
)
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
srvResource := &Resource{
|
||||
srvResource := &auth.Resource{
|
||||
Type: "service",
|
||||
Name: "go.micro.service.foo",
|
||||
Endpoint: "Foo.Bar",
|
||||
}
|
||||
|
||||
webResource := &Resource{
|
||||
webResource := &auth.Resource{
|
||||
Type: "service",
|
||||
Name: "go.micro.web.foo",
|
||||
Endpoint: "/foo/bar",
|
||||
}
|
||||
|
||||
catchallResource := &Resource{
|
||||
catchallResource := &auth.Resource{
|
||||
Type: "*",
|
||||
Name: "*",
|
||||
Endpoint: "*",
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
Error error
|
||||
Account *Account
|
||||
Resource *Resource
|
||||
Name string
|
||||
Rules []*Rule
|
||||
Rules []*auth.Rule
|
||||
Account *auth.Account
|
||||
Resource *auth.Resource
|
||||
Error error
|
||||
}{
|
||||
{
|
||||
Name: "NoRules",
|
||||
Rules: []*Rule{},
|
||||
Rules: []*auth.Rule{},
|
||||
Account: nil,
|
||||
Resource: srvResource,
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "CatchallPublicAccount",
|
||||
Account: &Account{},
|
||||
Account: &auth.Account{},
|
||||
Resource: srvResource,
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "",
|
||||
Resource: catchallResource,
|
||||
},
|
||||
@@ -51,8 +53,8 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "CatchallPublicNoAccount",
|
||||
Resource: srvResource,
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "",
|
||||
Resource: catchallResource,
|
||||
},
|
||||
@@ -60,10 +62,10 @@ func TestVerify(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "CatchallPrivateAccount",
|
||||
Account: &Account{},
|
||||
Account: &auth.Account{},
|
||||
Resource: srvResource,
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
},
|
||||
@@ -72,22 +74,22 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "CatchallPrivateNoAccount",
|
||||
Resource: srvResource,
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "CatchallServiceRuleMatch",
|
||||
Resource: srvResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: &Resource{
|
||||
Resource: &auth.Resource{
|
||||
Type: srvResource.Type,
|
||||
Name: srvResource.Name,
|
||||
Endpoint: "*",
|
||||
@@ -98,27 +100,27 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "CatchallServiceRuleNoMatch",
|
||||
Resource: srvResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: &Resource{
|
||||
Resource: &auth.Resource{
|
||||
Type: srvResource.Type,
|
||||
Name: "wrongname",
|
||||
Endpoint: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "ExactRuleValidScope",
|
||||
Resource: srvResource,
|
||||
Account: &Account{
|
||||
Account: &auth.Account{
|
||||
Scopes: []string{"neededscope"},
|
||||
},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "neededscope",
|
||||
Resource: srvResource,
|
||||
},
|
||||
@@ -127,58 +129,58 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "ExactRuleInvalidScope",
|
||||
Resource: srvResource,
|
||||
Account: &Account{
|
||||
Account: &auth.Account{
|
||||
Scopes: []string{"neededscope"},
|
||||
},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "invalidscope",
|
||||
Resource: srvResource,
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "CatchallDenyWithAccount",
|
||||
Resource: srvResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
Access: AccessDenied,
|
||||
Access: auth.AccessDenied,
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "CatchallDenyWithNoAccount",
|
||||
Resource: srvResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
Access: AccessDenied,
|
||||
Access: auth.AccessDenied,
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "RulePriorityGrantFirst",
|
||||
Resource: srvResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
Access: AccessGranted,
|
||||
Access: auth.AccessGranted,
|
||||
Priority: 1,
|
||||
},
|
||||
{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
Access: AccessDenied,
|
||||
Access: auth.AccessDenied,
|
||||
Priority: 0,
|
||||
},
|
||||
},
|
||||
@@ -186,29 +188,29 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "RulePriorityDenyFirst",
|
||||
Resource: srvResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
Access: AccessGranted,
|
||||
Access: auth.AccessGranted,
|
||||
Priority: 0,
|
||||
},
|
||||
{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: catchallResource,
|
||||
Access: AccessDenied,
|
||||
Access: auth.AccessDenied,
|
||||
Priority: 1,
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "WebExactEndpointValid",
|
||||
Resource: webResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: webResource,
|
||||
},
|
||||
@@ -217,27 +219,27 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "WebExactEndpointInalid",
|
||||
Resource: webResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: &Resource{
|
||||
Resource: &auth.Resource{
|
||||
Type: webResource.Type,
|
||||
Name: webResource.Name,
|
||||
Endpoint: "invalidendpoint",
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "WebWildcardEndpoint",
|
||||
Resource: webResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: &Resource{
|
||||
Resource: &auth.Resource{
|
||||
Type: webResource.Type,
|
||||
Name: webResource.Name,
|
||||
Endpoint: "*",
|
||||
@@ -248,11 +250,11 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "WebWildcardPathEndpointValid",
|
||||
Resource: webResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: &Resource{
|
||||
Resource: &auth.Resource{
|
||||
Type: webResource.Type,
|
||||
Name: webResource.Name,
|
||||
Endpoint: "/foo/*",
|
||||
@@ -263,24 +265,24 @@ func TestVerify(t *testing.T) {
|
||||
{
|
||||
Name: "WebWildcardPathEndpointInvalid",
|
||||
Resource: webResource,
|
||||
Account: &Account{},
|
||||
Rules: []*Rule{
|
||||
{
|
||||
Account: &auth.Account{},
|
||||
Rules: []*auth.Rule{
|
||||
&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: &Resource{
|
||||
Resource: &auth.Resource{
|
||||
Type: webResource.Type,
|
||||
Name: webResource.Name,
|
||||
Endpoint: "/bar/*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: ErrForbidden,
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
if err := VerifyAccess(tc.Rules, tc.Account, tc.Resource); err != tc.Error {
|
||||
if err := Verify(tc.Rules, tc.Account, tc.Resource); err != tc.Error {
|
||||
t.Errorf("Expected %v but got %v", tc.Error, err)
|
||||
}
|
||||
})
|
1512
auth/service/proto/auth.pb.go
Normal file
1512
auth/service/proto/auth.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user