├── .gitignore ├── .gitmodules ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_CN.md ├── ROADMAP.md ├── cmd ├── api │ └── api_server.go ├── backend │ └── backend.go └── proxy │ └── proxy.go ├── docker-compose.yml ├── docs-cn ├── api.md ├── apiserver.md ├── benchmark.md ├── build.md ├── cluster.md ├── images │ ├── bm_cpu_all.jpg │ ├── bm_cpu_io_sum.jpg │ └── bm_net_packet.png ├── optimization.md ├── plugin.md ├── plugin_js.md ├── proxy.md ├── restful.md ├── routing.md ├── server.md └── user-guide.md ├── docs ├── api.md ├── apiserver.md ├── benchmark.md ├── build.md ├── cluster.md ├── images │ ├── api_2.png │ ├── api_basics.png │ ├── bm_cpu_all.jpg │ ├── bm_cpu_io_sum.jpg │ ├── bm_net_packet.png │ ├── defaultFilters.png │ ├── jwt_example.png │ ├── jwt_in_auth.png │ ├── jwt_postman.png │ ├── postman.png │ ├── routing.png │ ├── server_configuration.png │ ├── specs.png │ └── web_ui_front_page.png ├── optimization.md ├── plugin-tutorial.md ├── plugin.md ├── plugin_js.md ├── proxy.md ├── restful.md ├── routing.md ├── server.md ├── tutorial.md └── user-guide.md ├── entrypoint.sh ├── examples ├── api.go ├── cluster.go ├── example.go ├── jwt.json ├── jwt_a_simple_working_one.json ├── routing.go └── server.go ├── go.mod ├── go.sum ├── grafana └── grafana.json ├── images ├── alipay.jpg ├── arch.png ├── flow.png ├── logo.png ├── qr.jpg └── wechat.png ├── pkg ├── client │ ├── api.go │ ├── client.go │ ├── cluster.go │ ├── plugin.go │ ├── routing.go │ └── server.go ├── expr │ ├── expr.go │ └── expr_test.go ├── filter │ ├── cache_util.go │ ├── const.go │ ├── filter.go │ └── test_help.go ├── lb │ ├── haship.go │ ├── haship_test.go │ ├── lb.go │ ├── rand.go │ ├── rand_test.go │ ├── roundrobin.go │ ├── weightrobin.go │ └── weightrobin_test.go ├── pb │ ├── gen.sh │ ├── metapb │ │ ├── metapb.pb.go │ │ └── metapb.proto │ ├── rpcpb │ │ ├── rpcpb.pb.go │ │ ├── rpcpb.proto │ │ └── services.go │ └── validation.go ├── plugin │ ├── builtin_base.go │ ├── builtin_http.go │ ├── builtin_json.go │ ├── builtin_log.go │ ├── builtin_redis.go │ ├── context_adapter.go │ ├── engine.go │ ├── runtime.go │ └── runtime_test.go ├── proxy │ ├── cfg.go │ ├── checker.go │ ├── dispatcher.go │ ├── dispatcher_copy_on_write.go │ ├── dispatcher_event.go │ ├── dispatcher_meta.go │ ├── dispatcher_runtime.go │ ├── errors.go │ ├── factory.go │ ├── filter.go │ ├── filter_access.go │ ├── filter_analysis.go │ ├── filter_blacklist.go │ ├── filter_caching.go │ ├── filter_circuit_breaker.go │ ├── filter_cross_domain.go │ ├── filter_headers.go │ ├── filter_jwt.go │ ├── filter_prepare.go │ ├── filter_rate_limiting.go │ ├── filter_validation.go │ ├── filter_whitelist.go │ ├── filter_xforwardfor.go │ ├── io.go │ ├── metric.go │ ├── multi.go │ ├── pool.go │ ├── proxy.go │ ├── proxy_gc.go │ ├── proxy_https.go │ ├── proxy_start.go │ ├── proxy_websocket.go │ ├── rate_limit.go │ └── render.go ├── route │ ├── const.go │ ├── lexer.go │ ├── parser.go │ ├── parser_test.go │ ├── route.go │ ├── route_test.go │ ├── scanner.go │ └── scanner_test.go ├── service │ ├── errors.go │ ├── g.go │ ├── http.go │ ├── http_api.go │ ├── http_bind.go │ ├── http_cluster.go │ ├── http_plugin.go │ ├── http_routing.go │ ├── http_server.go │ ├── http_static.go │ ├── http_system.go │ └── rpc_meta.go ├── store │ ├── store.go │ ├── store_etcd.go │ ├── store_watch.go │ └── txn.go └── util │ ├── addr.go │ ├── analysis.go │ ├── analysis_test.go │ ├── barrier.go │ ├── fasthttp_client.go │ ├── ip_util.go │ ├── lru.go │ ├── metric_push.go │ ├── time.go │ └── version.go └── prometheus.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | .vscode/* 10 | Library/ 11 | dist/ 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | cmd/api/api_server 29 | cmd/backend/backend 30 | cmd/proxy/proxy 31 | .idea/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor"] 2 | path = vendor 3 | url = https://github.com/fagongzi/gateway-vendor.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: true 3 | dist: xenial 4 | 5 | go: 6 | - 1.11 7 | - 1.12 8 | - 1.13 9 | 10 | script: go build ./... 11 | 12 | env: 13 | global: 14 | core: 15 | longpaths: true 16 | core.longpaths: true -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | ARG APP_ROOT=/app/manba 4 | ARG EXEC_NAME=manba-proxy 5 | ARG UID=2019 6 | ARG CMD_NAME=demo 7 | ENV CURRENT_EXEC_PATH=${APP_ROOT}/${EXEC_NAME} 8 | ENV PATH=${APP_ROOT}:$PATH 9 | 10 | WORKDIR ${APP_ROOT} 11 | 12 | ADD dist ${APP_ROOT} 13 | 14 | # Alpine Linux doesn't use pam, which means that there is no /etc/nsswitch.conf, 15 | # but Golang relies on /etc/nsswitch.conf to check the order of DNS resolving 16 | # (see https://github.com/golang/go/commit/9dee7771f561cf6aee081c0af6658cc81fac3918) 17 | # To fix this we just create /etc/nsswitch.conf and add the following line: 18 | # hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4 19 | 20 | RUN MAIN_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 0-2) \ 21 | && mv /etc/apk/repositories /etc/apk/repositories-bak \ 22 | && { \ 23 | echo "https://mirrors.aliyun.com/alpine/v${MAIN_VERSION}/main"; \ 24 | echo "https://mirrors.aliyun.com/alpine/v${MAIN_VERSION}/community"; \ 25 | } >> /etc/apk/repositories \ 26 | && apk add --update --no-cache libcap \ 27 | && addgroup -g ${UID} -S manba \ 28 | && adduser -u ${UID} -S manba -G manba \ 29 | && mkdir -p ${APP_ROOT}/plugins \ 30 | && chown -R manba:manba ./ \ 31 | && if [ -e ${CURRENT_EXEC_PATH} ]; then \ 32 | setcap CAP_NET_BIND_SERVICE=+eip ${CURRENT_EXEC_PATH}; \ 33 | fi \ 34 | && echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf \ 35 | && echo -n ${CMD_NAME} > cmd 36 | 37 | USER manba 38 | 39 | EXPOSE 80 2379 9092 9093 40 | 41 | ENTRYPOINT ["/bin/sh", "./entrypoint.sh"] 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RELEASE_VERSION = $(release_version) 2 | 3 | ifeq ("$(RELEASE_VERSION)","") 4 | RELEASE_VERSION := "unknown" 5 | endif 6 | 7 | ROOT_DIR = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/ 8 | VERSION_PATH = $(shell echo $(ROOT_DIR) | sed -e "s;${GOPATH}/src/;;g")pkg/util 9 | LD_GIT_COMMIT = -X '$(VERSION_PATH).GitCommit=`git rev-parse --short HEAD`' 10 | LD_BUILD_TIME = -X '$(VERSION_PATH).BuildTime=`date +%FT%T%z`' 11 | LD_GO_VERSION = -X '$(VERSION_PATH).GoVersion=`go version`' 12 | LD_MANBA_VERSION = -X '$(VERSION_PATH).Version=$(RELEASE_VERSION)' 13 | LD_FLAGS = -ldflags "$(LD_GIT_COMMIT) $(LD_BUILD_TIME) $(LD_GO_VERSION) $(LD_MANBA_VERSION) -w -s" 14 | 15 | GOOS = linux 16 | CGO_ENABLED = 0 17 | DIST_DIR = $(ROOT_DIR)dist/ 18 | 19 | ETCD_VER = v3.3.12 20 | ETCD_DOWNLOAD_URL = https://github.com/coreos/etcd/releases/download 21 | 22 | MY_TARGET := dist_dir 23 | EXEC_NAME := manba-proxy 24 | IMAGE_NAME := manba 25 | CMD_NAME := demo 26 | ifeq ("$(MAKECMDGOALS)","docker") 27 | ifeq ("$(with)","") 28 | MY_TARGET := release download_etcd ui 29 | endif 30 | ifeq ($(findstring etcd,$(with)),etcd) 31 | MY_TARGET := $(MY_TARGET) download_etcd 32 | EXEC_NAME := etcd 33 | IMAGE_NAME = etcd 34 | CMD_NAME = etcd 35 | endif 36 | ifeq ($(findstring apiserver,$(with)),apiserver) 37 | MY_TARGET := $(MY_TARGET) apiserver ui 38 | EXEC_NAME := apiserver 39 | IMAGE_NAME = apiserver 40 | CMD_NAME = apiserver 41 | endif 42 | ifeq ($(findstring proxy,$(with)),proxy) 43 | MY_TARGET := $(MY_TARGET) proxy 44 | EXEC_NAME := manba-proxy 45 | IMAGE_NAME = proxy 46 | CMD_NAME = manba-proxy 47 | endif 48 | endif 49 | 50 | .PHONY: release 51 | release: dist_dir apiserver proxy; 52 | 53 | .PHONY: release_darwin 54 | release_darwin: darwin dist_dir apiserver proxy; 55 | 56 | .PHONY: docker 57 | docker: 58 | @$(MAKE) $(MY_TARGET) 59 | @echo ========== current docker tag is: $(RELEASE_VERSION) ========== 60 | docker build -t fagongzi/$(IMAGE_NAME):$(RELEASE_VERSION) \ 61 | --build-arg EXEC_NAME="$(EXEC_NAME)" \ 62 | --build-arg CMD_NAME="$(CMD_NAME)" \ 63 | -f Dockerfile . 64 | docker tag fagongzi/$(IMAGE_NAME):$(RELEASE_VERSION) fagongzi/$(IMAGE_NAME) 65 | 66 | .PHONY: ui 67 | ui: ; $(info ======== compile ui:) 68 | git clone https://github.com/fagongzi/gateway-ui-vue.git $(DIST_DIR)ui 69 | cd $(DIST_DIR)ui && git checkout 3.0.0 70 | 71 | .PHONY: darwin 72 | darwin: 73 | $(eval GOOS := darwin) 74 | 75 | .PHONY: apiserver 76 | apiserver: ; $(info ======== compiled apiserver:) 77 | env CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) go build -mod vendor -a -installsuffix cgo -o $(DIST_DIR)manba-apiserver $(LD_FLAGS) $(ROOT_DIR)cmd/api/*.go 78 | 79 | .PHONY: proxy 80 | proxy: ; $(info ======== compiled proxy:) 81 | env CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) go build -mod vendor -a -installsuffix cgo -o $(DIST_DIR)manba-proxy $(LD_FLAGS) $(ROOT_DIR)cmd/proxy/*.go 82 | 83 | .PHONY: download_etcd 84 | download_etcd: 85 | curl -L $(ETCD_DOWNLOAD_URL)/$(ETCD_VER)/etcd-$(ETCD_VER)-linux-amd64.tar.gz -o /tmp/etcd-$(ETCD_VER)-linux-amd64.tar.gz 86 | tar xzvf /tmp/etcd-$(ETCD_VER)-linux-amd64.tar.gz -C $(DIST_DIR) --strip-components=1 87 | @rm -rf $(DIST_DIR)Documentation $(DIST_DIR)README* 88 | 89 | .PHONY: dist_dir 90 | dist_dir: ; $(info ======== prepare distribute dir:) 91 | mkdir -p $(DIST_DIR) 92 | @rm -rf $(DIST_DIR)* 93 | @cp entrypoint.sh $(DIST_DIR) 94 | 95 | .PHONY: clean 96 | clean: ; $(info ======== clean all:) 97 | rm -rf $(DIST_DIR)* 98 | 99 | .PHONY: help 100 | help: 101 | @echo "build release binary: \n\t\tmake release\n" 102 | @echo "build Mac OS X release binary: \n\t\tmake release_darwin\n" 103 | @echo "build docker release with etcd: \n\t\tmake docker\n" 104 | @echo "\t add 「with」 params can select what you need:\n" 105 | @echo "\t default: all, like 「make docker」\n" 106 | @echo "\t etcd: download and extract etcd and etcdctl\n" 107 | @echo "\t proxy: only compile proxy\n" 108 | @echo "\t apiserver: compile apiserver and download ui\n" 109 | @echo "clean all binary: \n\t\tmake clean\n" 110 | 111 | UNAME_S := $(shell uname -s) 112 | 113 | ifeq ($(UNAME_S),Darwin) 114 | .DEFAULT_GOAL := release_darwin 115 | else 116 | .DEFAULT_GOAL := release 117 | endif 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Gitter](https://badges.gitter.im/fagongzi/gateway.svg)](https://gitter.im/fagongzi/gateway?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | [![Build Status](https://api.travis-ci.org/fagongzi/gateway.svg)](https://travis-ci.org/fagongzi/gateway) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/fagongzi/gateway)](https://goreportcard.com/report/github.com/fagongzi/gateway) 6 | 7 | Manba/[简体中文](README_CN.md) 8 | ------- 9 | Manba is a restful API gateway based on HTTP, which can be used as a unified API access layer. 10 | 11 | ## Tutorial 12 | A very detailed tutorial for beginners. [Link](./docs/tutorial.md) 13 | Below are video tutorials. 14 | Basics: 15 | [![](https://img.youtube.com/vi/2qMWmdcw7o4/0.jpg)](https://www.youtube.com/watch?v=2qMWmdcw7o4) 16 | 17 | Alternative bilibili.com video link: https://www.bilibili.com/video/av73432556/ 18 | 19 | Routing Configuration Tutorial: 20 | [![](https://img.youtube.com/vi/D1pI6opB_ks/0.jpg)](https://www.youtube.com/watch?v=D1pI6opB_ks) 21 | 22 | Alternative bilibili.com video link: https://www.bilibili.com/video/av73432836/ 23 | 24 | JWT Plugin Configuration Tutorial: 25 | [![](https://img.youtube.com/vi/sLb16YDSlBs/0.jpg)](https://www.youtube.com/watch?v=sLb16YDSlBs) 26 | 27 | Alternative bilibili.com video link: https://www.bilibili.com/video/av73433002/ 28 | 29 | ## Attention 30 | Please make sure your Go version is 1.10 or above. Otherwise, **undefined "math/rand".Shuffle** error will occur when compiling. [StackOverFlow Link](https://stackoverflow.com/questions/52172794/getting-undefined-rand-shuffle-in-golang) 31 | 32 | 33 | ## Features 34 | * Traffic Control (on Server or API) 35 | * Circuit Breaker (on Server or API) 36 | * Load Balance 37 | * Service Discovery 38 | * Plugin 39 | * Routing (Divert Traffic, Duplicate Traffic) 40 | * API Aggregation 41 | * API Argument Check 42 | * API Access Control (White and Black List) 43 | * API Default Return Value 44 | * API Customized Return Value 45 | * API Result Cache 46 | * JWT Authorization 47 | * API Metric Imports Prometheus 48 | * API Retry After Failure 49 | * Backend Server Health Check 50 | * Open Management of API (GRPC、Restful) 51 | * Websocket Support 52 | * Online Data Migration Support 53 | 54 | ## Docker 55 | 56 | The following content requires reader some knowledge of Docker. You can refer to [this book][2], or check out [the official documentation][1]。 57 | 58 | ### Available Docker Images 59 | * `fagongzi/proxy` 60 | 61 | proxy component, `production ready` 62 | 63 | * `fagongzi/apiserver` 64 | 65 | apiserver component, `production ready` 66 | 67 | ### Quick start with docker-compose 68 | ```bash 69 | docker-compose up -d 70 | ``` 71 | 72 | Use `http://127.0.0.1:9093/ui/index.html` to access `apiserver` 73 | 74 | Use `http://127.0.0.1` to access to your API 75 | 76 | ## Architecture 77 | ![](./images/arch.png) 78 | 79 | ## Web UI 80 | Available Manba Web UI Projects: 81 | * [Official](https://github.com/fagongzi/gateway-ui-vue) 82 | * [gateway_ui (v2.x ONLY)](https://github.com/archfish/gateway_ui) 83 | * [gateway_admin_ui](https://github.com/wilehos/gateway_admin_ui) 84 | 85 | ## Components 86 | Manba consists of `proxy` and `apiserver`. 87 | 88 | ### Proxy 89 | Proxy is a component which provides service to clients. Proxy is a stateless node. Multiple proxies can be deployed to handle huge traffic. 90 | [More](./docs/proxy.md). 91 | 92 | ### ApiServer 93 | ApiServer provides GRPC and Restful to manage metadata for users. ApiServer integrates official Web UI. 94 | [More](./docs/apiserver.md). 95 | 96 | ## Concepts of Manba 97 | ### Server 98 | A server is a a real backend service. 99 | [More](./docs/server.md). 100 | 101 | ### Cluster 102 | Cluster consists of servers which provide the same service. A server is chosen to handle a specific request based on a load balance strategy. 103 | [More](./docs/cluster.md). 104 | 105 | ### API 106 | API is a key concept of Manba. We can manage external APIs in Manba and their distribution rules, aggregation rules and URL matching rules. 107 | [More](./docs/api.md). 108 | 109 | ### Routing 110 | Routing is a route strategy. Cookie, Querystring, Header and Path in HTTP Request dictate traffic distribution and traffic duplication to a specific cluster. Through this feature, AB test and online traffic divertion is achieved. 111 | [More](./docs/routing.md). 112 | 113 | ## Getting Involved 114 | [More](./docs/build.md) 115 | 116 | ## WeChat 117 | ![](./images/qr.jpg) 118 | 119 | [1]: https://docs.docker.com/ "Docker Documentation" 120 | [2]: https://github.com/yeasy/docker_practice "docker_practice" 121 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Gitter](https://badges.gitter.im/fagongzi/gateway.svg)](https://gitter.im/fagongzi/gateway?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | [![Build Status](https://api.travis-ci.org/fagongzi/gateway.svg)](https://travis-ci.org/fagongzi/gateway) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/fagongzi/gateway)](https://goreportcard.com/report/github.com/fagongzi/gateway) 6 | 7 | Manba/[English](./README.md) 8 | ------- 9 | Manba是一个基于HTTP协议的restful的API网关。可以作为统一的API接入层。 10 | 11 | ## 教程 12 | 如果你是一个初学者,那么这个[详细的教程](./docs/tutorial.md)非常适合你。现在只有英文版本。 13 | 14 | ## 注意 15 | 请确保你的Go版本是在1.10或者之上。用1.10之前版本的Go编译时会出现**undefined "math/rand".Shuffle**错误。[StackOverFlow链接](https://stackoverflow.com/questions/52172794/getting-undefined-rand-shuffle-in-golang) 16 | 17 | ## Features 18 | * 流量控制(Server或API级别) 19 | * 熔断(Server或API级别) 20 | * 负载均衡 21 | * 服务发现 22 | * 插件机制 23 | * 路由(分流,复制流量) 24 | * API 聚合 25 | * API 参数校验 26 | * API 访问控制(黑白名单) 27 | * API 默认返回值 28 | * API 定制返回值 29 | * API 结果Cache 30 | * JWT Authorization 31 | * API Metric导入Prometheus 32 | * API 失败重试 33 | * 后端server的健康检查 34 | * 开放管理API(GRPC、Restful) 35 | * 支持websocket 36 | * 支持在线迁移数据 37 | 38 | ## Docker 39 | 40 | 以下内容要求对docker基本操作有一定了解,可以看[这本书][2],或者直接看[官方文档][1]。 41 | 42 | ### Quick start with docker-compose 43 | ```bash 44 | docker-compose up -d 45 | ``` 46 | 47 | 使用 `http://127.0.0.1:9093/ui/index.html` 访问 `apiserver` 48 | 49 | 使用 `http://127.0.0.1` 访问你的API 50 | 51 | ## 架构 52 | ![](./images/arch.png) 53 | 54 | ## WebUI 55 | 可用的Manba的WebUI的项目: 56 | * [官方](https://github.com/fagongzi/gateway-ui-vue) 57 | * [gateway_ui(仅适配2.x)](https://github.com/archfish/gateway_ui) 58 | * [gateway_admin_ui](https://github.com/wilehos/gateway_admin_ui) 59 | 60 | ## 组件 61 | Gateway由`proxy`, `apiserver`组成 62 | 63 | ### Proxy 64 | Proxy是Gateway对终端用户提供服务的组件,Proxy是一个无状态的节点,可以部署多个来支撑更大的流量,[更多](./docs-cn/proxy.md)。 65 | 66 | ### ApiServer 67 | ApiServer对外提供GRPC和Restful来管理元信息,ApiServer同时集成了官方的WebUI,[更多](./docs-cn/apiserver.md)。 68 | 69 | ## Manba中的概念 70 | ### Server 71 | Server是一个真实的后端服务,[更多](./docs-cn/server.md)。 72 | 73 | ### Cluster 74 | Cluster是一个逻辑概念,它由一组提供相同服务的Server组成。会依据负载均衡策略选择一个可用的Server,[更多](./docs-cn/cluster.md)。 75 | 76 | ### API 77 | API是Manba的核心概念,我们可以在Manba的中维护对外的API,以及API的分发规则,聚合规则以及URL匹配规则,[更多](./docs-cn/api.md)。 78 | 79 | ### Routing 80 | Routing是一个路由策略,根据HTTP Request中的Cookie,Querystring、Header、Path中的一些信息把流量分发到或者复制到指定的Cluster,通过这个功能,我们可以实现AB Test和线上引流,[更多](./docs-cn/routing.md)。 81 | 82 | ## 参与开发 83 | [更多](./docs-cn/build.md) 84 | 85 | ## 交流方式-微信 86 | ![](./images/qr.jpg) 87 | 88 | [1]: https://docs.docker.com/ "Docker Documentation" 89 | [2]: https://github.com/yeasy/docker_practice "docker_practice" 90 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 这个文档定义了Manba的roadmap. 3 | 4 | ## Features 5 | - [x] 在线流量复制 6 | - [x] 定制化返回值 7 | - [x] API结果Cache 8 | - [x] API Server支持Restful 9 | - [x] 支持依赖聚合 10 | - [ ] 协议转换插件机制 11 | 12 | ## Plugins 13 | - [x] JWT插件 14 | - [ ] 分布式限流插件 15 | - [ ] SpringCloud协议转换插件 16 | - [ ] Dubbo协议转换插件 17 | - [ ] Grpc协议转换插件 18 | -------------------------------------------------------------------------------- /cmd/api/api_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "runtime" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/fagongzi/gateway/pkg/util" 13 | 14 | "github.com/coreos/etcd/clientv3" 15 | "github.com/fagongzi/gateway/pkg/pb/rpcpb" 16 | "github.com/fagongzi/gateway/pkg/service" 17 | "github.com/fagongzi/gateway/pkg/store" 18 | "github.com/fagongzi/grpcx" 19 | "github.com/fagongzi/log" 20 | "github.com/labstack/echo" 21 | "google.golang.org/grpc" 22 | ) 23 | 24 | var ( 25 | addr = flag.String("addr", "127.0.0.1:9092", "Addr: client grpc entrypoint") 26 | addrHTTP = flag.String("addr-http", "127.0.0.1:9093", "Addr: client http restful entrypoint") 27 | addrStore = flag.String("addr-store", "etcd://127.0.0.1:2379", "Addr: store address") 28 | addrStoreUser = flag.String("addr-store-user", "", "addr Store UserName") 29 | addrStorePwd = flag.String("addr-store-pwd", "", "addr Store Password") 30 | namespace = flag.String("namespace", "dev", "The namespace to isolation the environment.") 31 | discovery = flag.Bool("discovery", false, "Publish apiserver service via discovery.") 32 | servicePrefix = flag.String("service-prefix", "/services", "The prefix for service name.") 33 | publishLease = flag.Int64("publish-lease", 10, "Publish service lease seconds") 34 | publishTimeout = flag.Int("publish-timeout", 30, "Publish service timeout seconds") 35 | ui = flag.String("ui", "/app/manba/ui/dist", "The gateway ui dist dir.") 36 | uiPrefix = flag.String("ui-prefix", "/ui", "The gateway ui prefix path.") 37 | version = flag.Bool("version", false, "Show version info") 38 | ) 39 | 40 | func main() { 41 | flag.Parse() 42 | 43 | if *version { 44 | util.PrintVersion() 45 | os.Exit(0) 46 | } 47 | 48 | log.InitLog() 49 | runtime.GOMAXPROCS(runtime.NumCPU()) 50 | 51 | log.Infof("addr: %s", *addr) 52 | log.Infof("addr-store: %s", *addrStore) 53 | log.Infof("addr-store-user: %s", *addrStoreUser) 54 | log.Infof("addr-store-pwd: %s", *addrStorePwd) 55 | log.Infof("namespace: %s", *namespace) 56 | log.Infof("discovery: %v", *discovery) 57 | log.Infof("service-prefix: %s", *servicePrefix) 58 | log.Infof("publish-lease: %d", *publishLease) 59 | log.Infof("publish-timeout: %d", *publishTimeout) 60 | 61 | db, err := store.GetStoreFrom(*addrStore, fmt.Sprintf("/%s", *namespace), *addrStoreUser, *addrStorePwd) 62 | if err != nil { 63 | log.Fatalf("init store failed for %s, errors:\n%+v", 64 | *addrStore, 65 | err) 66 | } 67 | 68 | service.Init(db) 69 | 70 | var opts []grpcx.ServerOption 71 | if *discovery { 72 | opts = append(opts, grpcx.WithEtcdPublisher(db.Raw().(*clientv3.Client), *servicePrefix, *publishLease, time.Second*time.Duration(*publishTimeout))) 73 | } 74 | 75 | if *addrHTTP != "" { 76 | opts = append(opts, grpcx.WithHTTPServer(*addrHTTP, func(server *echo.Echo) { 77 | service.InitHTTPRouter(server, *ui, *uiPrefix) 78 | })) 79 | } 80 | 81 | s := grpcx.NewGRPCServer(*addr, func(svr *grpc.Server) []grpcx.Service { 82 | var services []grpcx.Service 83 | rpcpb.RegisterMetaServiceServer(svr, service.MetaService) 84 | services = append(services, grpcx.NewService(rpcpb.ServiceMeta, nil)) 85 | return services 86 | }, opts...) 87 | 88 | log.Infof("api server listen at %s", *addr) 89 | go s.Start() 90 | 91 | waitStop(s) 92 | } 93 | 94 | func waitStop(s *grpcx.GRPCServer) { 95 | sc := make(chan os.Signal, 1) 96 | signal.Notify(sc, 97 | syscall.SIGHUP, 98 | syscall.SIGINT, 99 | syscall.SIGTERM, 100 | syscall.SIGQUIT) 101 | 102 | sig := <-sc 103 | s.GracefulStop() 104 | log.Infof("exit: signal=<%d>.", sig) 105 | switch sig { 106 | case syscall.SIGTERM: 107 | log.Infof("exit: bye :-).") 108 | os.Exit(0) 109 | default: 110 | log.Infof("exit: bye :-(.") 111 | os.Exit(1) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /cmd/backend/backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/fagongzi/util/format" 11 | "github.com/labstack/echo" 12 | md "github.com/labstack/echo/middleware" 13 | ) 14 | 15 | var ( 16 | addr = flag.String("addr", "127.0.0.1:9090", "addr for backend") 17 | ) 18 | 19 | func main() { 20 | flag.Parse() 21 | 22 | server := echo.New() 23 | server.Use(md.Logger()) 24 | server.Use(md.Gzip()) 25 | 26 | server.GET("/serverinfo", func(c echo.Context) error { 27 | hostname, _ := os.Hostname() 28 | return c.String(http.StatusOK, hostname+"\n"+*addr) 29 | }) 30 | server.GET("/fail", func(c echo.Context) error { 31 | sleep := c.QueryParam("sleep") 32 | if sleep != "" { 33 | time.Sleep(time.Second * time.Duration(format.MustParseStrInt(sleep))) 34 | } 35 | 36 | code := c.QueryParam("code") 37 | if code != "" { 38 | return c.String(format.MustParseStrInt(code), "OK") 39 | } 40 | 41 | return c.String(http.StatusOK, "OK") 42 | }) 43 | 44 | server.GET("/check", func(c echo.Context) error { 45 | return c.String(http.StatusOK, "OK") 46 | }) 47 | 48 | server.GET("/header", func(c echo.Context) error { 49 | name := c.QueryParam("name") 50 | return c.String(http.StatusOK, c.Request().Header.Get(name)) 51 | }) 52 | 53 | server.GET("/host", func(c echo.Context) error { 54 | return c.String(http.StatusOK, "Host in HTTP request header: "+c.Request().Host+"\nserver:"+*addr) 55 | }) 56 | 57 | server.GET("/error", func(c echo.Context) error { 58 | return c.NoContent(http.StatusBadRequest) 59 | }) 60 | 61 | server.GET("/v1/components/:id", func(c echo.Context) error { 62 | value := make(map[string]interface{}) 63 | data := make(map[string]interface{}) 64 | user := make(map[string]interface{}) 65 | user["id"] = c.Param("id") 66 | user["name"] = fmt.Sprintf("v1-name-%s", c.Param("id")) 67 | data["user"] = user 68 | data["source"] = *addr 69 | data["query"] = c.QueryString() 70 | 71 | value["code"] = "0" 72 | value["data"] = data 73 | return c.JSON(http.StatusOK, value) 74 | }) 75 | server.GET("/v1/users/:id", func(c echo.Context) error { 76 | user := make(map[string]interface{}) 77 | user["id"] = c.Param("id") 78 | user["name"] = fmt.Sprintf("v1-name-%s", c.Param("id")) 79 | user["source"] = *addr 80 | user["query"] = c.QueryString() 81 | user["header"] = c.QueryParam(c.Request().Header.Get("header")) 82 | return c.JSON(http.StatusOK, user) 83 | }) 84 | server.GET("/v1/account/:id", func(c echo.Context) error { 85 | account := make(map[string]interface{}) 86 | account["id"] = c.Param("id") 87 | account["source"] = *addr 88 | account["account"] = fmt.Sprintf("v1-account-%s", c.Param("id")) 89 | account["query"] = c.QueryString() 90 | return c.JSON(http.StatusOK, account) 91 | }) 92 | 93 | server.GET("/v2/users/:id", func(c echo.Context) error { 94 | user := make(map[string]interface{}) 95 | user["id"] = c.Param("id") 96 | user["source"] = *addr 97 | user["name"] = fmt.Sprintf("v2-name-%s", c.Param("id")) 98 | user["query"] = c.QueryString() 99 | return c.JSON(http.StatusOK, user) 100 | }) 101 | server.GET("/v2/account/:id", func(c echo.Context) error { 102 | account := make(map[string]interface{}) 103 | account["id"] = c.Param("id") 104 | account["source"] = *addr 105 | account["account"] = fmt.Sprintf("v2-account-%s", c.Param("id")) 106 | account["query"] = c.QueryString() 107 | return c.JSON(http.StatusOK, account) 108 | }) 109 | 110 | server.Start(*addr) 111 | } 112 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | services: 3 | pushgateway: 4 | image: prom/pushgateway:v0.9.1 5 | expose: 6 | - 9091 7 | ports: 8 | - "9091:9091" 9 | 10 | prometheus: 11 | image: prom/prometheus:v2.9.2 12 | depends_on: 13 | - pushgateway 14 | volumes: 15 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 16 | command: 17 | - --config.file=/etc/prometheus/prometheus.yml 18 | expose: 19 | - 9090 20 | ports: 21 | - "9090:9090" 22 | 23 | grafana: 24 | image: grafana/grafana:4.6.3 25 | depends_on: 26 | - prometheus 27 | environment: 28 | - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin} 29 | - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin} 30 | - GF_USERS_ALLOW_SIGN_UP=false 31 | expose: 32 | - 3000 33 | ports: 34 | - "3000:3000" 35 | 36 | etcd: 37 | image: gcr.io/etcd-development/etcd 38 | depends_on: 39 | - prometheus 40 | expose: 41 | - 2379 42 | ports: 43 | - "2379:2379" 44 | command: 45 | - etcd 46 | - --listen-client-urls=http://0.0.0.0:2379 47 | - --advertise-client-urls=http://0.0.0.0:2379 48 | 49 | apiserver: 50 | image: fagongzi/apiserver 51 | depends_on: 52 | - etcd 53 | expose: 54 | - 9093 55 | ports: 56 | - "9093:9093" 57 | command: 58 | - manba-apiserver 59 | - --addr-http=:9093 60 | - --addr-store=etcd://etcd:2379 61 | 62 | proxy: 63 | image: fagongzi/proxy 64 | depends_on: 65 | - etcd 66 | - apiserver 67 | expose: 68 | - 80 69 | ports: 70 | - "80:80" 71 | command: 72 | - manba-proxy 73 | - --addr=:80 74 | - --addr-store=etcd://etcd:2379 75 | - --metric-job=proxy 76 | - --metric-address=pushgateway:9091 77 | - --interval-metric-sync=5 78 | - --js 79 | -------------------------------------------------------------------------------- /docs-cn/apiserver.md: -------------------------------------------------------------------------------- 1 | ApiServer 2 | -------------- 3 | ApiServer对外提供GRPC接口,用来管理Manba的元信息(Cluster、Server、Routing以及API)。 4 | 5 | # 对外开放的接口: 6 | ``` 7 | // MetaService is a interface for meta manager 8 | service MetaService { 9 | rpc PutCluster (PutClusterReq) returns (PutClusterRsp) {} 10 | rpc RemoveCluster (RemoveClusterReq) returns (RemoveClusterRsp) {} 11 | rpc GetCluster (GetClusterReq) returns (GetClusterRsp) {} 12 | rpc GetClusterList (GetClusterListReq) returns (stream metapb.Cluster) {} 13 | 14 | rpc PutServer (PutServerReq) returns (PutServerRsp) {} 15 | rpc RemoveServer (RemoveServerReq) returns (RemoveServerRsp) {} 16 | rpc GetServer (GetServerReq) returns (GetServerRsp) {} 17 | rpc GetServerList (GetServerListReq) returns (stream metapb.Server) {} 18 | 19 | rpc PutAPI (PutAPIReq) returns (PutAPIRsp) {} 20 | rpc RemoveAPI (RemoveAPIReq) returns (RemoveAPIRsp) {} 21 | rpc GetAPI (GetAPIReq) returns (GetAPIRsp) {} 22 | rpc GetAPIList (GetAPIListReq) returns (stream metapb.API) {} 23 | 24 | rpc PutRouting (PutRoutingReq) returns (PutRoutingRsp) {} 25 | rpc RemoveRouting (RemoveRoutingReq) returns (RemoveRoutingRsp) {} 26 | rpc GetRouting (GetRoutingReq) returns (GetRoutingRsp) {} 27 | rpc GetRoutingList (GetRoutingListReq) returns (stream metapb.Routing) {} 28 | 29 | rpc AddBind (AddBindReq) returns (AddBindRsp) {} 30 | rpc RemoveBind (RemoveBindReq) returns (RemoveBindRsp) {} 31 | rpc RemoveClusterBind (RemoveClusterBindReq) returns (RemoveClusterBindRsp) {} 32 | rpc GetBindServers (GetBindServersReq) returns (GetBindServersRsp) {} 33 | } 34 | ``` 35 | 具体的PB在项目的`pkg/pb/rpcpb`目录下 36 | 37 | # 客户端 38 | 目前Gateway支持GO的客户端,这里以Gateway的GO客户端管理元信息的例子,参见[examples](../examples) 39 | -------------------------------------------------------------------------------- /docs-cn/cluster.md: -------------------------------------------------------------------------------- 1 | Cluster 2 | ------- 3 | 在Manba中,Cluster是一个逻辑的概念。它是后端真实Server的一个逻辑组,在同一个组内的后端Server提供相同的服务。 4 | 5 | # Cluster属性 6 | 一个Cluster包含2部分信息: 7 | ## ID 8 | Cluster ID, 全局唯一。 9 | 10 | ## Name 11 | Cluster名称 12 | 13 | ## LoadBalance 14 | Cluster采取的负载均衡算法。 15 | -------------------------------------------------------------------------------- /docs-cn/images/bm_cpu_all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs-cn/images/bm_cpu_all.jpg -------------------------------------------------------------------------------- /docs-cn/images/bm_cpu_io_sum.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs-cn/images/bm_cpu_io_sum.jpg -------------------------------------------------------------------------------- /docs-cn/images/bm_net_packet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs-cn/images/bm_net_packet.png -------------------------------------------------------------------------------- /docs-cn/optimization.md: -------------------------------------------------------------------------------- 1 | ## Optimization 2 | Manba网关在使用的时候有一些地方需要做一些优化,以便达到更好的性能。这里给出一些优化配置的意见。 3 | 4 | ### Docker 5 | 在使用docker启动`Proxy`镜像的时候,由于默认的镜像的`somaxconn`是默认值`128`,docker 启动的时候加上如下参数: 6 | ```bash 7 | docker run -d --rm --sysctl net.core.somaxconn=你期望的值 fagongzi/proxy 8 | ``` 9 | 10 | ### Keepalive 11 | Proxy默认回启用Keepalive来保持和后端服务的链接,这里有2个重要的启动参数: 12 | * limit-conn-keepalive 链接最大Keepalive保持的时间 13 | * limit-conn-idle 链接最大的空闲时间,超过这个时间,Proxy会主动关闭这个连接 14 | 15 | 建议配置: `后端服务的Keepalive时间 > limit-conn-keepalive > 2*limit-conn-idle` 16 | 17 | ### limit-dispatch 18 | 这个参数是用来设置`聚合`请求的工作协程池的个数,当系统中有`聚合`请求,可以适当调大这个值来提升`聚合`请求的吞吐。 19 | 20 | ### limit-copy 21 | 这个参数是用来设置`Copy流量`流量的工作协程池的个数,当系统中有`Copy流量`流量的场景,可以适当调大这个值来提升`Copy流量`的吞吐。 22 | 23 | ### limit-conn 24 | 这个参数是用来控制`Proxy`与每个后端建立的最大链接数,如果发现某个后端Server的并发非常高,延迟较大,有可能时间花费了等待空闲链接的操作上,可以适当调大这个值。 25 | 26 | ### limit-caching 27 | 这个参数控制单个`Proxy`使用多少内存来做缓存,如果`Proxy`中的API开启了缓存,那么可以通过这个参数控制缓存大小。 28 | -------------------------------------------------------------------------------- /docs-cn/plugin.md: -------------------------------------------------------------------------------- 1 | Filter plugin 2 | -------------- 3 | Manba中的很多功能都是使用Filter来实现的,用户的大部分功能需求都可以使用Filter来解决。所以Filter被设计成Plugin机制,借助于Go1.8的plugin机制,可以很好的扩展Manba。 4 | 5 | # Request处理流程 6 | request -> filter预处理 -> 转发请求 -> filter后置处理 -> 响应客户端 7 | 8 | 整个逻辑处理符合以下规则: 9 | 10 | * filter预处理返回错误,流程立即终止,并且使用filter返回的状态码响应客户端 11 | * filter后置处理返回错误,使用filter返回的状态码响应客户端 12 | * 转发请求,后端返回的状态码`>=500`,调用filter的错误处理接口 13 | 14 | # Filter接口定义 15 | ```golang 16 | // Filter filter interface 17 | type Filter interface { 18 | Name() string 19 | Init(cfg string) error 20 | 21 | Pre(c Context) (statusCode int, err error) 22 | Post(c Context) (statusCode int, err error) 23 | PostErr(c Context) 24 | } 25 | 26 | // Context filter context 27 | type Context interface { 28 | StartAt() time.Time 29 | EndAt() time.Time 30 | 31 | OriginRequest() *fasthttp.RequestCtx 32 | ForwardRequest() *fasthttp.Request 33 | Response() *fasthttp.Response 34 | 35 | API() *metapb.API 36 | DispatchNode() *metapb.DispatchNode 37 | Server() *metapb.Server 38 | Analysis() *util.Analysis 39 | 40 | SetAttr(key string, value interface{}) 41 | GetAttr(key string) interface{} 42 | } 43 | 44 | // BaseFilter base filter support default implemention 45 | type BaseFilter struct{} 46 | 47 | // Pre execute before proxy 48 | func (f BaseFilter) Pre(c Context) (statusCode int, err error) { 49 | return http.StatusOK, nil 50 | } 51 | 52 | // Post execute after proxy 53 | func (f BaseFilter) Post(c Context) (statusCode int, err error) { 54 | return http.StatusOK, nil 55 | } 56 | 57 | // PostErr execute proxy has errors 58 | func (f BaseFilter) PostErr(c Context) { 59 | 60 | } 61 | ``` 62 | 63 | 这些相关的定义都在`github.com/fagongzi/manba/pkg/filter`包中,每一个Filter都需要导入。其中的`Context`的上下文接口,提供了Filter和Manba交互的能力;`BaseFilter`定义了默认行为。 64 | 65 | # Manba加载Filter插件机制 66 | ```golang 67 | func newExternalFilter(filterSpec *conf.FilterSpec) (filter.Filter, error) { 68 | p, err := plugin.Open(filterSpec.ExternalPluginFile) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | s, err := p.Lookup("NewExternalFilter") 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | sf := s.(func() (filter.Filter, error)) 79 | return sf() 80 | } 81 | ``` 82 | 83 | 每一个外部的Filter插件,对外提供`NewExternalFilter`,返回一个`filter.Filter`实现,或者错误。 84 | 85 | # Go1.8 Plugin的问题 86 | 当编写的自定义插件的时候,有一个问题涉及到Go1.8的一个[Bug](https://github.com/golang/go/issues/19233)。所以编写的自定义插件必须在`Manba的Project`下编译的插件才能被正确加载。 87 | 88 | # Go1.9.2以上版本 89 | 支持插件项目独立目录,但是不能有自己的vender目录,否则加载的时候一样会出现1.8的问题。 90 | 91 | # 自定义插件例子 92 | 可以参考这个例子实现自己的插件 93 | [参考JWT插件](https://github.com/fagongzi/jwt-plugin) 94 | 95 | # 启动自定义插件 96 | `Proxy`组件有一个`--filter`选项来指定Manba使用的插件以及顺序。默认情况下Manba使用一下的内置插件顺序:`--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION`。例如我们开发好了一个插件JWT,并且编译成为jwt.so文件,可以加上启动参数加载插件:`--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION --filter JWT:/plugins/jwt.so:/plugins/jwt.json`,自定义插件的格式:`名称:插件文件:插件配置` 97 | -------------------------------------------------------------------------------- /docs-cn/proxy.md: -------------------------------------------------------------------------------- 1 | Proxy 2 | -------------- 3 | Proxy是一个独立的进程,对外提供网关的代理服务,Proxy是无状态的,可以水平扩容,提升服务能力。 4 | 5 | # 职责 6 | Proxy的负责接收和响应客户端的http请求,可以作为后端服务的统一接入层。 7 | 8 | # Proxy的处理请求的流程 9 | ![](../images/flow.png) -------------------------------------------------------------------------------- /docs-cn/routing.md: -------------------------------------------------------------------------------- 1 | Routing 2 | ------ 3 | 在Manba中,Routing代表一个路由,利用路由我们可以实现我们的AB Test以及线上导流等高级特性。 4 | 5 | # Routing属性 6 | ## ID 7 | Routing ID,唯一标识 8 | 9 | ## Name 10 | Routing Name,路由名称 11 | 12 | ## clusterID 13 | 流量路由到哪一个Cluster 14 | 15 | ## apiID 16 | 针对哪一个API设置路由 17 | 18 | ## Condition(可选) 19 | 路由条件,当满足这些条件,则Manba执行这个路由。路由条件可以设置`cookie`、`querystring`、`header`、`json body`,`path value`中的参数的表达式。不配置,匹配所有流量。 20 | 21 | ## RoutingStrategy 22 | 路由策略,目前支持`Split`分发。分发是指:把满足条件的请求按照比例转发到目标Cluster,剩余比例的流量按照正常流程进入API匹配阶段,流向原有的Cluster。 23 | 24 | ## TrafficRate 25 | 路由流量的比例,例如设置为50,那么50%的流量会根据`RoutingStrategy`进行路由。 26 | 27 | ## Status 28 | 路由的状态,只有`UP`状态才会生效。 29 | -------------------------------------------------------------------------------- /docs-cn/server.md: -------------------------------------------------------------------------------- 1 | Server 2 | ------ 3 | 在Manba中,一个Server对应一个真实存在的后端Server。 4 | 5 | # Server属性 6 | ## ID 7 | Server ID,唯一标识。 8 | 9 | ## Addr 10 | Server地址,格式为:"IP:PORT"。 11 | 12 | ## Protocol 13 | Server的接口协议,目前支持HTTP。 14 | 15 | ## Weight 16 | Weight 服务器的权重(当该服务器所属的集群负载方式是权重轮询时则需要配置) 17 | 18 | ## MaxQPS 19 | Server能够支持的最大QPS,用于流控。Manba采用令牌桶算法,根据QPS限制流量,保护后端Server被压垮。 20 | 21 | ## HealthCheck(可选) 22 | Server的健康检查机制,目前支持HTTP的协议检查,支持检查返回状态码以及返回内容。如果没有设置,认为这个Server的健康检查交给外部,Manba永久认为这个Server是健康的。 23 | 24 | ## CircuitBreaker(可选) 25 | 熔断器,设置后端Server的熔断规则。熔断器分为3个状态: 26 | 27 | * Open 28 | 29 | Open状态,正常状态,Manba放入全部流量。当Manba发现失败的请求比例达到了设置的规则,熔断器会把状态切换到Close状态 30 | 31 | * Half 32 | 33 | Half状态,尝试恢复的状态。在这个状态下,Manba会尝试放入一定比例的流量,然后观察这些流量的请求的情况,如果达到预期就把状态转换为Open状态,如果没有达到预期,恢复成Close状态 34 | 35 | * Close 36 | 37 | Close状态,在这个状态下,Manba禁止任何流量进入这个后端Server,在达到指定的阈值时间后,Manba自动尝试切换到Half状态,尝试恢复。 38 | -------------------------------------------------------------------------------- /docs/apiserver.md: -------------------------------------------------------------------------------- 1 | ApiServer 2 | -------------- 3 | ApiServer provides GRPC APIs to manage metadata of Gateway which is Cluster,Server, Routing, and API info. 4 | 5 | # APIs Exposed 6 | ``` 7 | // MetaService is a interface for meta manager 8 | service MetaService { 9 | rpc PutCluster (PutClusterReq) returns (PutClusterRsp) {} 10 | rpc RemoveCluster (RemoveClusterReq) returns (RemoveClusterRsp) {} 11 | rpc GetCluster (GetClusterReq) returns (GetClusterRsp) {} 12 | rpc GetClusterList (GetClusterListReq) returns (stream metapb.Cluster) {} 13 | 14 | rpc PutServer (PutServerReq) returns (PutServerRsp) {} 15 | rpc RemoveServer (RemoveServerReq) returns (RemoveServerRsp) {} 16 | rpc GetServer (GetServerReq) returns (GetServerRsp) {} 17 | rpc GetServerList (GetServerListReq) returns (stream metapb.Server) {} 18 | 19 | rpc PutAPI (PutAPIReq) returns (PutAPIRsp) {} 20 | rpc RemoveAPI (RemoveAPIReq) returns (RemoveAPIRsp) {} 21 | rpc GetAPI (GetAPIReq) returns (GetAPIRsp) {} 22 | rpc GetAPIList (GetAPIListReq) returns (stream metapb.API) {} 23 | 24 | rpc PutRouting (PutRoutingReq) returns (PutRoutingRsp) {} 25 | rpc RemoveRouting (RemoveRoutingReq) returns (RemoveRoutingRsp) {} 26 | rpc GetRouting (GetRoutingReq) returns (GetRoutingRsp) {} 27 | rpc GetRoutingList (GetRoutingListReq) returns (stream metapb.Routing) {} 28 | 29 | rpc AddBind (AddBindReq) returns (AddBindRsp) {} 30 | rpc RemoveBind (RemoveBindReq) returns (RemoveBindRsp) {} 31 | rpc RemoveClusterBind (RemoveClusterBindReq) returns (RemoveClusterBindRsp) {} 32 | rpc GetBindServers (GetBindServersReq) returns (GetBindServersRsp) {} 33 | } 34 | ``` 35 | The PB is under `pkg/pb/rpcpb`. 36 | 37 | # Client 38 | For the moment, Gateway supports Go client. Here are [examples](../examples) of Go clients of Gateway which manage metadata. 39 | -------------------------------------------------------------------------------- /docs/cluster.md: -------------------------------------------------------------------------------- 1 | Cluster 2 | ------- 3 | In Gateway, Cluster is a logical concept. It is a logical collection of backend servers which provide the same service. 4 | 5 | # Cluster Attributes 6 | A Cluster has two fields. 7 | ## ID 8 | Cluster ID, unique identifier 9 | 10 | ## Name 11 | Cluster Name 12 | 13 | ## LoadBalance 14 | The load balance algorithm used by Cluster -------------------------------------------------------------------------------- /docs/images/api_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/api_2.png -------------------------------------------------------------------------------- /docs/images/api_basics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/api_basics.png -------------------------------------------------------------------------------- /docs/images/bm_cpu_all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/bm_cpu_all.jpg -------------------------------------------------------------------------------- /docs/images/bm_cpu_io_sum.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/bm_cpu_io_sum.jpg -------------------------------------------------------------------------------- /docs/images/bm_net_packet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/bm_net_packet.png -------------------------------------------------------------------------------- /docs/images/defaultFilters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/defaultFilters.png -------------------------------------------------------------------------------- /docs/images/jwt_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/jwt_example.png -------------------------------------------------------------------------------- /docs/images/jwt_in_auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/jwt_in_auth.png -------------------------------------------------------------------------------- /docs/images/jwt_postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/jwt_postman.png -------------------------------------------------------------------------------- /docs/images/postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/postman.png -------------------------------------------------------------------------------- /docs/images/routing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/routing.png -------------------------------------------------------------------------------- /docs/images/server_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/server_configuration.png -------------------------------------------------------------------------------- /docs/images/specs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/specs.png -------------------------------------------------------------------------------- /docs/images/web_ui_front_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/web_ui_front_page.png -------------------------------------------------------------------------------- /docs/optimization.md: -------------------------------------------------------------------------------- 1 | ## Optimization 2 | There are some places where the Gateway Gateway needs to be optimized to achieve better performance. Here are some suggestions for optimizing the configuration. 3 | 4 | ### Docker 5 | When using docker to start the `Proxy` image, since the default image `somaxconn` is the default value of `128`, the docker starts with the following parameters: 6 | ```bash 7 | Docker run -d --rm --sysctl net.core.somaxconn=The value you expect fagongzi/proxy 8 | ``` 9 | 10 | ### Keepalive 11 | Proxy defaults to enabling Keepalive to maintain links to backend services. There are 2 important startup parameters: 12 | * limit-conn-keepalive link maximum keepalive time 13 | * limit-conn-idle The maximum idle time of the link. After this time, the Proxy will actively close the connection. 14 | 15 | Recommended configuration: `Keepalive time of backend service> limit-conn-keepalive > 2*limit-conn-idle` 16 | 17 | ### limit-dispatch 18 | This parameter is used to set the number of working coroutine pools for the ʻaggregation` request. When there is an 'aggregation' request in the system, this value can be appropriately adjusted to improve the throughput of the `aggregation` request. 19 | 20 | ### limit-copy 21 | This parameter is used to set the number of working coordination pools of `Copy traffic` traffic. When there is a scene of `Copy traffic` traffic in the system, this value can be adjusted appropriately to improve the throughput of `Copy traffic`. 22 | 23 | ### limit-conn 24 | This parameter is used to control the maximum number of links that `Proxy` can establish with each backend. If it finds that the backend of a backend server is very high and the delay is large, it may take time to wait for the operation of the idle link. Increase this value. 25 | 26 | ### limit-caching 27 | This parameter controls how much memory a single `Proxy` uses for caching. If the API in `Proxy` has caching enabled, then this parameter can be used to control the cache size. -------------------------------------------------------------------------------- /docs/plugin-tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | This tutorial aims to teach you how to make your own plugins. 3 | If you just started dealing with Gateway and want to add a plugin to fulfill your deployment needs, then congradulations, you have come to the right place! 4 | I have gone through a lot of pain trying to figure out how to write my JWT plugin. And thanks to the in-time replies of the maintainer and other members of the community, I finally made it. I wrapped up my weary and yet fruitful journey and published this tutorial in the hope that future newcomers have a detailed reference to look up. 5 | If you encounter any problem going through the tutorial, feel free to create an issue or send me an email to this address **brucewangno1@qq.com** with the subject "Issues with Gateway Tutorial." 6 | 7 | ## JWT Plugin Example 8 | Before we dive into anything, I should tell you that **pkg/proxy/filter_jwt.go** is a properly working one with methods **Pre()** and **Post()**. You may modify it to suit your needs. 9 | **examples/jwt_a_simple_working_one.json**, as its name suggests, is a simple working configuration which has one functionality - to check if the JWT is in the Redis server. 10 | Add an JWT entry to your Redis server by **redis-cli**: 11 | ```redis 12 | SET prefix_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.G9-vbFqv8vkn91T028YcVOVId-wUF9kG5wIc2UgXxe4 true 13 | ``` 14 | **prefix_** is in consistency with that in the JSON configuration file. 15 | 16 | Start your proxy server with a command like the following: 17 | ```shell 18 | ./proxy --addr=127.0.0.1:80 --addr-rpc=127.0.0.1:9091 --addr-store=etcd://127.0.0.1:2379 --namespace=test --filter JWT --jwt "/some/path/leading/to/jwt_a_simple_working_one.json" 19 | ``` 20 | ![](images/jwt_example.png) 21 | "this_is_jwt_secret" which appears in the JSON file is in the signature field in this JWT example. 22 | ![](images/jwt_in_auth.png) 23 | Before we mock an API request from the client side, we have to go to the UI page and add this entry to take this all into effect. FYI, this makes **c.API().AuthFilter** in the **Pre()** method in **pkg/proxy/filter_jwt.go** **JWT** which matches **f.Name()**. 24 | 25 | ![](images/jwt_postman.png) 26 | Alright, alright, alright. This picture is self-explanatory. 27 | 28 | ## Customize JWT Plugin 29 | Since we are developing a JWT plugin to authenticate external requests, most of us have already had some working knowledge of JWT. If not, [this official introduction](https://jwt.io/introduction/) is pretty informative. For Chinese developers, [this link from Ruan Yifeng's blog](http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) is an excellent concise alternative. 30 | 31 | ### Source Code Walk-through 32 | **pkg/filter/filter.go** is pretty much an empty abstract from which **\*Filters** structs in **pkg/proxy/filter_\*.go** inherit the **BaseFilter** struct. For example, struct **ValidationFilter** only implementes methods **Init()**, **Name()**, and **Pre()**. These methods in **BaseFilter** are overridden and others remain available. 33 | For JWT plugins, you should name your file like **filter_my_plugin.go** under **pkg/proxy/** and refer to **filter_plugin.go** to write your own. 34 | **\*.so** plugin as mentioned in the [docs/plugin.md](plugin.md), which involves **cgo** is really troublesome and thus highly discouraged. 35 | After you have finished your plugin file, a plugin JSON configuration file is needed. For more information, please reference to [this JSON configuration file example](https://github.com/fagongzi/jwt-plugin). 36 | For your JWT plugin to take effect, you need to pass options **--filter JWT** and **--jwt yourJSONConfigurationFilePath**. This is because neither **filter_jwt.go** nor your plugin is in **defaultFilters** in **pkg/proxy/proxy.go**. 37 | An example JWT JSON configuration file can be found in [**examples/jwt.json**](../examples/jwt.json). 38 | ![](./images/defaultFilters.png) 39 | If one of the **defaultFilters** is expected to be used, please specify it by **--filter** like **--filter WHITELIST** when launching **proxy** because if there is **--filter**, **defaultFilters** gets discarded. 40 | ![](./images/specs.png) 41 | 42 | -------------------------------------------------------------------------------- /docs/plugin.md: -------------------------------------------------------------------------------- 1 | Filter plugin 2 | -------------- 3 | Most features of Gateway are implemented through Filter. Most of users' functional requirements are implemented through Filter. Filter is thus implemented as a plugin thanks to Go 1.8's plugin mechanism to scale Gateway well. 4 | 5 | # Handling Procedures of Request 6 | request -> filter preprocess -> redirect request -> filter postprocess -> respond client 7 | 8 | All the logic processing follows the rules below: 9 | 10 | * When filter preprocessing returns error, the procedure aborts immediately and uses the returned status code of filter to respond to client. 11 | * When filter postprocessing returns error, the returned status code of filter is used to respond to client. 12 | * When the status code of the response of redirected requests is `>=500`, filter's error handling API is called. 13 | 14 | # Filter API Definition 15 | ```golang 16 | // Filter filter interface 17 | type Filter interface { 18 | Name() string 19 | Init(cfg string) error 20 | 21 | Pre(c Context) (statusCode int, err error) 22 | Post(c Context) (statusCode int, err error) 23 | PostErr(c Context) 24 | } 25 | 26 | // Context filter context 27 | type Context interface { 28 | StartAt() time.Time 29 | EndAt() time.Time 30 | 31 | OriginRequest() *fasthttp.RequestCtx 32 | ForwardRequest() *fasthttp.Request 33 | Response() *fasthttp.Response 34 | 35 | API() *metapb.API 36 | DispatchNode() *metapb.DispatchNode 37 | Server() *metapb.Server 38 | Analysis() *util.Analysis 39 | 40 | SetAttr(key string, value interface{}) 41 | GetAttr(key string) interface{} 42 | } 43 | 44 | // BaseFilter base filter support default implemention 45 | type BaseFilter struct{} 46 | 47 | // Pre execute before proxy 48 | func (f BaseFilter) Pre(c Context) (statusCode int, err error) { 49 | return http.StatusOK, nil 50 | } 51 | 52 | // Post execute after proxy 53 | func (f BaseFilter) Post(c Context) (statusCode int, err error) { 54 | return http.StatusOK, nil 55 | } 56 | 57 | // PostErr execute proxy has errors 58 | func (f BaseFilter) PostErr(c Context) { 59 | 60 | } 61 | ``` 62 | 63 | Relevant definitions are in `github.com/fagongzi/gateway/pkg/filter`. Each filter needs to be imported. `Context` API provides the ability of interactions between Filter and Gateway. `BaseFilter` defines the default. 64 | 65 | # The Mechanism of Gateway Loading Filter Plugin 66 | ```golang 67 | func newExternalFilter(filterSpec *conf.FilterSpec) (filter.Filter, error) { 68 | p, err := plugin.Open(filterSpec.ExternalPluginFile) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | s, err := p.Lookup("NewExternalFilter") 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | sf := s.(func() (filter.Filter, error)) 79 | return sf() 80 | } 81 | ``` 82 | 83 | Every external Filter plugin exposes `NewExternalFilter`. It returns a `filter.Filter` or an error. 84 | 85 | # A Problem of Go1.8 Plugin 86 | When writing customized plugin, there is a problem concerning a Go 1.8 [bug](https://github.com/golang/go/issues/19233). It can be avoided by compiling the customized plugin under `Gateway/Project` in order to make the plugin load correctly. 87 | 88 | # For Go 1.9.2 and Above 89 | Independent directories of plugins are supported. However, it can not have its own vender directory, otherwise, the same problem arises as with Go 1.8 90 | 91 | # A Customized Plugin Example 92 | You can refer to this example to make your own plugin 93 | [JWT Plugin Reference](https://github.com/fagongzi/jwt-plugin) 94 | 95 | # Start A Customized Plugin Example 96 | `Proxy` component has a `--filter` option to designate plugins used by Gateway and its order. By default, the order of built-in plugins of Gateway:`--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION`. For instance, suppose we have a JWT plugin ready and it is compiled as a jwt.so file, options to start can be `--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION --filter JWT:/plugins/jwt.so:/plugins/jwt.json`. The format of a customized plugin is `Name:Plugin File:Plugin Configuration` -------------------------------------------------------------------------------- /docs/proxy.md: -------------------------------------------------------------------------------- 1 | Proxy 2 | -------------- 3 | Proxy is an independent process which provides gateway proxy service. Proxy is stateless and able to scale horizontally to enhance service. 4 | 5 | # Responsibility 6 | Proxy accepts and responds to HTTP requests from clients. It can be the unified access layer of backend services. 7 | 8 | # Request Handling Procedure of Proxy 9 | ![](../images/flow.png) -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | Routing 2 | ------ 3 | A Routing represents a router. Through routing, we can implement AB test, traffic direction and other advanced features. 4 | 5 | # Routing Attributes 6 | ## ID 7 | Unique Identifier 8 | 9 | ## Name 10 | Routing Name 11 | 12 | ## clusterID 13 | The cluster to which the routing traffic goes. 14 | 15 | ## apiID 16 | The API for which the routing is 17 | 18 | ## Condition (Optional) 19 | Routing Condition. When the condition is met, Gateway executes this routing strategy. The routing condition can set the arguement expressions of `cookie`、`querystring`、`header`、`json body`,`path value`. If not set, all traffic is matched. 20 | 21 | ## RoutingStrategy 22 | Currently support `Split`, which refers to redirecting a certain percentage of eligible requests to the target cluster and direct the rest to the API matching phase and then to the original cluster destination. 23 | 24 | ## TrafficRate 25 | If set to 50, 50% of traffic is being routed according to `RoutingStrategy`. 26 | 27 | ## Status 28 | Routing is valid only if status is `UP`. -------------------------------------------------------------------------------- /docs/server.md: -------------------------------------------------------------------------------- 1 | Server 2 | ------ 3 | In Gateway, a server refers to a real backend server. 4 | 5 | # Server Attributes 6 | ## ID 7 | Unique Identifier 8 | 9 | ## Addr 10 | Format: "IP:PORT" 11 | 12 | ## Protocol 13 | API Protocol. Currently only support HTTP 14 | 15 | ## Weight 16 | Valid only if the load balance strategy is Weighted Round Robin 17 | 18 | ## MaxQPS 19 | Maximum QPS supported by server. Used to Control Traffic. Gateway uses the Token Bucket Algorithm, restricting traffic by MaxQPS, thus protecting backend servers from overload. 20 | 21 | ## HealthCheck (Optional) 22 | Health check mechanism, currently supporting HTTP check, response status code and response body. If not set, the server's health check becomes external responsibility and Gateway always assumes that this server is healthy. 23 | 24 | ## CircuitBreaker (Optional) 25 | Backend server circuit break status: 26 | 27 | * Open 28 | 29 | Normal. All traffic in. When Gateway find the failed requests to all requests ratio reach a certain threshold, CircuitBreaker switches from Open to Close. 30 | 31 | * Half 32 | 33 | Attempt to recover. Gateway tries to direct a certain percentage of traffic to the server and observe the result. If the expectation is met, CircuitBreaker switches to Open. If not, Close. 34 | 35 | * Close 36 | 37 | Gateway does not direct any traffic to this backend server. When the time threshold is reached, Gateway automatically tries to recover by switching to Half. -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | If you have not dealt much with HTTP gateway before and want to deploy one in your company or just play with one, then congradulations, you have come to the right place! 3 | This tutorial is very user-friendly. It aims to assist first-timers to have a hands-on experience without going through the pain of searching, asking and wondering. 4 | This was tested on a **MacOS** environment. 5 | If you encounter any problem going through the tutorial, feel free to create an issue or send me an email to this address **brucewangno1@qq.com** with the subject "Issues with Gateway Tutorial." 6 | 7 | ## ETCD Setup 8 | ETCD is a distributed key-value storage required to store **Gateway configurations**. 9 | You need an ETCD server or a cluster of them running. 10 | Follow the directions on [this page](https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/container.md#docker). 11 | 12 | ## Gateway Setup 13 | ### Download this project 14 | Under **$GOPATH/src/github.com/fagongzi**, run 15 | ```shell 16 | git clone https://github.com/fagongzi/gateway.git 17 | ``` 18 | 19 | ### Compile this project 20 | Make sure your Go version is **1.10** or above. Otherwise error will occurs. 21 | In the root directory of this project, there is a **Makefile** which is responsible to generate the executables and the static Web UI. Under the root directory of this project, run 22 | ```shell 23 | make 24 | ``` 25 | to generate executables **apiserver** and **proxy** , found under directory **dist**; run 26 | ```shell 27 | make ui 28 | ``` 29 | to generate the UI directory under dist, which is the Web UI needed by **apiserver**. 30 | 31 | ## Gateway Service Online 32 | ### Run executables 33 | So now there is a ETCD or a cluster of ETCD Docker containers running. 34 | Under directory **dist**, run the following two command lines in two separate terminal tabs in a termial (**iTerm** highly recommended) 35 | ```shell 36 | sudo ./proxy --addr=127.0.0.1:80 --addr-rpc=127.0.0.1:9091 --addr-store=etcd://127.0.0.1:2379 --namespace=test 37 | ./apiserver --addr=127.0.0.1:9091 --addr-store=etcd://127.0.0.1:2379 --discovery --namespace=test -ui=ui/dist 38 | ``` 39 | to start proxy and apiserver. 40 | 41 | ## Backend Mock Service 42 | ### Start three servers 43 | Under the directory **cmd/backend**, there is the file **backend.go**. This is a backend mock service. It have many simple APIs like the one returning the hostname, ip and port of the server. 44 | Start up 3 terminal tabs and run the following commands in these 3 tabs, respectively. 45 | ```shell 46 | go run backend.go --addr=localhost:9000 47 | go run backend.go --addr=localhost:9001 48 | go run backend.go --addr=localhost:9002 49 | ``` 50 | Alright, alright, alright. You now have three servers which provide the same service and will later form a cluster by configurations on the Web UI. 51 | 52 | ## Gateway Configuration 53 | There are two ways to configure Gateway which is responsible for stuff like redirecting traffic. The first one is through Web UI and the second is through GRPC. 54 | 55 | ### Through the Web UI 56 | Web UI is at http://localhost:9093/ui/index.html#/home. Please do not try to access http://localhost:9093. Instead of the Web UI showing up, you get 57 | ```json 58 | {"message":"Not Found"} 59 | ``` 60 | A rookie mistake. In the future, this issue might be resolved. 61 | 62 | #### Web UI front page: 63 | ![](./images/web_ui_front_page.png) 64 | 1. Click on "Cluster" on the side bar and add a cluster. Let's name it "Cluster Server Info". For now the only load balance algorithm you could choose is "Round Robin". 65 | 2. Click on "Server" on the side bar and add the three servers you just started. QPS stands for Requests Per Second. 1000 is enough for our tutorial. /check is used by a health check mechanism. 66 | ![](./images/server_configuration.png) 67 | 3. Click on "API" on the side bar and add an API. 68 | ![](./images/api_basics.png) 69 | The retry strategy is to retry sending a request to a cluster after 10ms if it failed last time. 70 | ![](./images/api_2.png) 71 | 72 | The configuration is all set up. 73 | 74 | ## Postman 75 | ### Download Postman 76 | Click on [the link](https://www.getpostman.com/downloads/) to open the download page. 77 | 78 | ### Create a HTTP Get request 79 | Remember to add the **Host** field in **Headers**. Otherwise it will not work. 80 | ![](./images/postman.png) 81 | 82 | ### Click On "Send" 83 | And voila! You have something like 84 | ```html 85 | Johns-MacBook-Pro.local 86 | localhost:9002 87 | ``` 88 | Notice that almost every time you click on **Send**, server address changes. This is because of the Round Robin load balance strategy. 89 | 90 | ## Bonus Round 91 | ### Routing Strategy 92 | 1. Create three more servers with ip:ports: "localhost:9003", "localhost:9004", "localhost:9005". 93 | 2. Create a cluster called "Cluster Server Info Backup" 94 | 3. Click on "Routing" on the side bar and add a routing. Our **Split** routing strategy reroutes 60% of all the API traffic initially bound to **Cluster Server Info** to our designated cluster **Cluster Server Info Backup**. 95 | ![](./images/routing.png) 96 | If you keep clicking on "Send" enough times, you will find that approximately 60% of all your HTTP Get requests goes to the second cluster. 97 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | start_etcd() { 6 | etcd $ETCD_OPTS & 7 | } 8 | 9 | DEFAULT_IP="0.0.0.0" 10 | 11 | start_apiserver() { 12 | manba-apiserver --addr=${DEFAULT_IP}:9092 --addr-http=${DEFAULT_IP}:9093 --discovery $API_SERVER_OPTS & 13 | } 14 | 15 | INPUT_CMD=$@ 16 | CMD=`cat cmd` 17 | if [ "$INPUT_CMD" = "" ] 18 | then 19 | INPUT_CMD=${CMD} 20 | fi 21 | 22 | DEFAULT_EXEC="manba-proxy --addr=${DEFAULT_IP}:80 --log-level=$MANBA_LOG_LEVEL $GW_PROXY_OPTS" 23 | if [ "${INPUT_CMD}" = 'demo' ] 24 | then 25 | start_etcd 26 | sleep 3 27 | start_apiserver 28 | sleep 1 29 | EXEC=$DEFAULT_EXEC 30 | fi 31 | 32 | if [ "${INPUT_CMD}" = 'proxy' ] 33 | then 34 | EXEC=$DEFAULT_EXEC 35 | fi 36 | 37 | if [ "${INPUT_CMD}" = 'apiserver' ] 38 | then 39 | EXEC="apiserver --addr=${DEFAULT_IP}:9092 --addr-http=${DEFAULT_IP}:9093 --discovery $API_SERVER_OPTS" 40 | fi 41 | 42 | if [ "${INPUT_CMD}" = 'etcd' ] 43 | then 44 | EXEC="etcd $ETCD_OPTS" 45 | fi 46 | 47 | if [ ! -z "${INPUT_CMD}" ] && [ -z "$EXEC" ] 48 | then 49 | EXEC=${INPUT_CMD} 50 | fi 51 | 52 | exec $EXEC 53 | -------------------------------------------------------------------------------- /examples/cluster.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fagongzi/gateway/pkg/pb/metapb" 7 | ) 8 | 9 | func createCluster() error { 10 | c, err := getClient() 11 | if err != nil { 12 | return err 13 | } 14 | 15 | id, err := c.NewClusterBuilder().Name("cluster-01").Loadbalance(metapb.RoundRobin).Commit() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | fmt.Printf("cluster id is: %d", id) 21 | return nil 22 | } 23 | 24 | func deleteCluster(id uint64) error { 25 | c, err := getClient() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return c.RemoveCluster(id) 31 | } 32 | 33 | func updateCluster(id uint64) error { 34 | c, err := getClient() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | cluster, err := c.GetCluster(id) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // 修改名称 45 | _, err = c.NewClusterBuilder().Use(*cluster).Name("cluster-1").Commit() 46 | if err != nil { 47 | return err 48 | } 49 | 50 | fmt.Printf("cluster %d name is updated", id) 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /examples/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/fagongzi/gateway/pkg/client" 7 | ) 8 | 9 | func main() { 10 | 11 | } 12 | 13 | // 如果你的api server使用了"--discovery"参数启动 14 | func getClientWithDiscovery() (client.Client, error) { 15 | return client.NewClientWithEtcdDiscovery("/services", 16 | time.Second*10, 17 | "127.0.0.1:2379") 18 | } 19 | 20 | // 如果你的api server没有使用"--discovery"参数启动 21 | func getClient() (client.Client, error) { 22 | return client.NewClient(time.Second*10, 23 | "127.0.0.1:9092") 24 | } 25 | -------------------------------------------------------------------------------- /examples/jwt.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret": "jwt secret", 3 | "method": "jwt signing method, [HS256|HS384|HS512]", 4 | "tokenLookup": "token lookup, [header|query|cookie:Authorization]", 5 | "authSchema": "jwt schema, [Bearer]", 6 | "renewTokenHeaderName": "the header name for new token in the response header", 7 | "csrfHeaderName": "the header name for CSRFToken", 8 | "redis": { 9 | "addr": "127.0.0.1:6379", 10 | "maxActive": "max connections, int", 11 | "maxIdle": "max idle connections, int", 12 | "idleTimeout": "idle timeout seconds, int" 13 | }, 14 | "actions": [ 15 | { 16 | "method": "token_in_redis", 17 | "params": { 18 | "prefix": "the prefix of token in the redis" 19 | } 20 | }, 21 | { 22 | "method": "token_and_csrf_in_redis", 23 | "params": { 24 | "prefix": "the prefix of token in the redis", 25 | "csrf_white_method":"GET,OPTION", 26 | "csrf_white_path":"/testinfo,/testdesc" 27 | } 28 | }, 29 | { 30 | "method": "renew_by_redis", 31 | "params": { 32 | "prefix": "the prefix of token in the redis", 33 | "ttl": "ttl seconds, int" 34 | } 35 | }, 36 | { 37 | "method": "renew_by_raw", 38 | "params": { 39 | "ttl": "ttl seconds, required if the renew is true, int" 40 | } 41 | }, 42 | { 43 | "method": "fetch_to_header", 44 | "params": { 45 | "prefix": "prefix added to field set to header", 46 | "fields": [ 47 | "f1", 48 | "f2" 49 | ] 50 | } 51 | }, 52 | { 53 | "method": "fetch_to_cookie", 54 | "params": { 55 | "prefix": "prefix added to field set to cookie", 56 | "fields": [ 57 | "f1", 58 | "f2" 59 | ] 60 | } 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /examples/jwt_a_simple_working_one.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret": "this_is_jwt_secret", 3 | "method": "HS256", 4 | "tokenLookup": "header:Authorization", 5 | "authSchema": "Bearer", 6 | "renewTokenHeaderName": "", 7 | "csrfHeaderName": "_csrf", 8 | "redis": { 9 | "addr": "127.0.0.1:6379", 10 | "maxActive": 1000, 11 | "maxIdle": 1000, 12 | "idleTimeout": 100 13 | }, 14 | "actions": [ 15 | { 16 | "method": "token_in_redis", 17 | "params": { 18 | "prefix": "prefix_" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/routing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | ) 6 | 7 | func createRouting() error { 8 | c, err := getClient() 9 | if err != nil { 10 | return err 11 | } 12 | 13 | rb := c.NewRoutingBuilder() 14 | // 必选项 15 | // 下线 16 | rb.Down() 17 | // 上线 18 | rb.Up() 19 | // 拆分10%的流量到cluster 2,剩余90%的流量按照原有规则转发 20 | rb.TrafficRate(10) 21 | rb.To(2) 22 | rb.Strategy(metapb.Split) 23 | 24 | // 可选项 25 | // 目标请求必须包含 v 的query string,且必须是v1,那么就是把v1的流量导流10%到cluster 2 26 | param := metapb.Parameter{ 27 | Name: "v", 28 | Source: metapb.QueryString, 29 | } 30 | rb.AddCondition(param, metapb.CMPEQ, "v1") 31 | 32 | _, err = rb.Commit() 33 | return err 34 | } 35 | 36 | func updateRouting(id uint64) error { 37 | c, err := getClient() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | routing, err := c.GetRouting(id) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | rb := c.NewRoutingBuilder().Use(*routing) 48 | // 必选项 49 | // 下线 50 | rb.Down() 51 | // 上线 52 | rb.Up() 53 | // 拆分10%的流量到cluster 2,剩余90%的流量按照原有规则转发 54 | rb.TrafficRate(10) 55 | rb.To(2) 56 | rb.Strategy(metapb.Split) 57 | 58 | // 可选项 59 | // 目标请求必须包含 v 的query string,且必须是v1,那么就是把v1的流量导流10%到cluster 2 60 | param := metapb.Parameter{ 61 | Name: "v", 62 | Source: metapb.QueryString, 63 | } 64 | rb.AddCondition(param, metapb.CMPEQ, "v1") 65 | 66 | _, err = rb.Commit() 67 | return err 68 | } 69 | -------------------------------------------------------------------------------- /examples/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func createServer() error { 9 | c, err := getClient() 10 | if err != nil { 11 | return err 12 | } 13 | 14 | sb := c.NewServerBuilder() 15 | // 必选项 16 | sb.Addr("127.0.0.1:8080").HTTPBackend().MaxQPS(100) 17 | 18 | // 健康检查,可选项 19 | // 每个10秒钟检查一次,每次检查的超时时间30秒,即30秒后端Server没有返回认为后端不健康 20 | sb.CheckHTTPCode("/check/path", time.Second*10, time.Second*30) 21 | 22 | // 熔断器,可选项 23 | // 统计周期1秒钟 24 | sb.CircuitBreakerCheckPeriod(time.Second) 25 | // 在Close状态60秒后自动转到Half状态 26 | sb.CircuitBreakerCloseToHalfTimeout(time.Second * 60) 27 | // Half状态下,允许10%的流量流入后端 28 | sb.CircuitBreakerHalfTrafficRate(10) 29 | // 在Half状态,1秒内有2%的请求失败了,转换到Close状态 30 | sb.CircuitBreakerHalfToCloseCondition(2) 31 | // 在Half状态,1秒内有90%的请求成功了,转换到Open状态 32 | sb.CircuitBreakerHalfToOpenCondition(90) 33 | 34 | id, err := sb.Commit() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | fmt.Printf("server id is: %d", id) 40 | 41 | // 把这个server加入到cluster 1 42 | c.AddBind(1, id) 43 | 44 | // 把这个server从cluster 1 移除 45 | c.RemoveBind(1, id) 46 | 47 | // 加入到cluster 2 48 | c.AddBind(2, id) 49 | return nil 50 | } 51 | 52 | func updateServer(id uint64) error { 53 | c, err := getClient() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | svr, err := c.GetServer(id) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | sb := c.NewServerBuilder() 64 | sb.Use(*svr) 65 | 66 | // 修改你想要修改的字段 67 | sb.MaxQPS(1000) 68 | sb.NoCircuitBreaker() // 删除熔断器 69 | sb.NoHeathCheck() // 删除健康检查 70 | 71 | _, err = sb.Commit() 72 | return err 73 | } 74 | 75 | func deleteServer(id uint64) error { 76 | c, err := getClient() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return c.RemoveServer(id) 82 | } 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fagongzi/gateway 2 | 3 | require ( 4 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect 5 | github.com/boltdb/bolt v1.3.1 // indirect 6 | github.com/buger/jsonparser v0.0.0-20180318095312-2cac668e8456 7 | github.com/coreos/bbolt v1.3.0 // indirect 8 | github.com/coreos/etcd v3.3.12+incompatible 9 | github.com/coreos/go-semver v0.2.0 // indirect 10 | github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect 11 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect 12 | github.com/dgrijalva/jwt-go v0.0.0-20180308231308-06ea1031745c 13 | github.com/fagongzi/goetty v0.0.0-20180427060148-8f06d410550f 14 | github.com/fagongzi/grpcx v0.0.0-20190226052515-f1ec50ae76bf 15 | github.com/fagongzi/log v0.0.0-20170831135209-9a647df25e0e 16 | github.com/fagongzi/util v0.0.0-20180330021808-4acf02da76a9 17 | github.com/garyburd/redigo v0.0.0-20180228092057-a69d19351219 18 | github.com/ghodss/yaml v1.0.0 // indirect 19 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 20 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 21 | github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect 22 | github.com/golang/protobuf v0.0.0-20180430185241-b4deda0973fb 23 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect 24 | github.com/gorilla/websocket v0.0.0-20180816221803-3ff3320c2a17 25 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect 26 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 27 | github.com/grpc-ecosystem/grpc-gateway v1.6.2 // indirect 28 | github.com/jonboulle/clockwork v0.1.0 // indirect 29 | github.com/juju/ratelimit v1.0.1 30 | github.com/koding/websocketproxy v0.0.0-20180716164433-0fa3f994f6e7 31 | github.com/labstack/echo v0.0.0-20180412143600-6d227dfea4d2 32 | github.com/labstack/gommon v0.0.0-20180613044413-d6898124de91 // indirect 33 | github.com/mattn/go-colorable v0.0.0-20170801030607-167de6bfdfba // indirect 34 | github.com/mattn/go-isatty v0.0.0-20170925053441-0360b2af4f38 // indirect 35 | github.com/matttproud/golang_protobuf_extensions v0.0.0-20160424113007-c12348ce28de // indirect 36 | github.com/pkg/errors v0.8.1 // indirect 37 | github.com/prometheus/client_golang v0.0.0-20160817154824-c5b7fccd2042 38 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect 39 | github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1 40 | github.com/prometheus/procfs v0.0.0-20180705121852-ae68e2d4c00f // indirect 41 | github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d 42 | github.com/sirupsen/logrus v1.2.0 // indirect 43 | github.com/soheilhy/cmux v0.0.0-20180129155001-e09e9389d85d 44 | github.com/stretchr/testify v1.2.2 45 | github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect 46 | github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect 47 | github.com/valyala/fasthttp v1.2.0 48 | github.com/valyala/fastrand v1.0.0 49 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect 50 | github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect 51 | go.uber.org/atomic v1.3.2 // indirect 52 | go.uber.org/multierr v1.1.0 // indirect 53 | go.uber.org/zap v1.9.1 // indirect 54 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 55 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect 56 | golang.org/x/text v0.3.2 // indirect 57 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect 58 | google.golang.org/genproto v0.0.0-20180716172848-2731d4fa720b // indirect 59 | google.golang.org/grpc v0.0.0-20180619221905-168a6198bcb0 60 | gopkg.in/sourcemap.v1 v1.0.5 // indirect 61 | gopkg.in/yaml.v2 v2.2.2 // indirect 62 | ) 63 | 64 | go 1.13 65 | -------------------------------------------------------------------------------- /images/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/alipay.jpg -------------------------------------------------------------------------------- /images/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/arch.png -------------------------------------------------------------------------------- /images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/flow.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/logo.png -------------------------------------------------------------------------------- /images/qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/qr.jpg -------------------------------------------------------------------------------- /images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/wechat.png -------------------------------------------------------------------------------- /pkg/client/cluster.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb" 5 | "github.com/fagongzi/gateway/pkg/pb/metapb" 6 | "github.com/fagongzi/gateway/pkg/pb/rpcpb" 7 | ) 8 | 9 | // ClusterBuilder cluster builder 10 | type ClusterBuilder struct { 11 | c *client 12 | value metapb.Cluster 13 | } 14 | 15 | // NewClusterBuilder return a cluster build 16 | func (c *client) NewClusterBuilder() *ClusterBuilder { 17 | return &ClusterBuilder{ 18 | c: c, 19 | value: metapb.Cluster{}, 20 | } 21 | } 22 | 23 | // Use use a cluster 24 | func (cb *ClusterBuilder) Use(value metapb.Cluster) *ClusterBuilder { 25 | cb.value = value 26 | return cb 27 | } 28 | 29 | // Name set a name 30 | func (cb *ClusterBuilder) Name(name string) *ClusterBuilder { 31 | cb.value.Name = name 32 | return cb 33 | } 34 | 35 | // Loadbalance set a loadbalance 36 | func (cb *ClusterBuilder) Loadbalance(lb metapb.LoadBalance) *ClusterBuilder { 37 | cb.value.LoadBalance = lb 38 | return cb 39 | } 40 | 41 | // Commit commit 42 | func (cb *ClusterBuilder) Commit() (uint64, error) { 43 | err := pb.ValidateCluster(&cb.value) 44 | if err != nil { 45 | return 0, err 46 | } 47 | 48 | return cb.c.putCluster(cb.value) 49 | } 50 | 51 | // Build build 52 | func (cb *ClusterBuilder) Build() (*rpcpb.PutClusterReq, error) { 53 | err := pb.ValidateCluster(&cb.value) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return &rpcpb.PutClusterReq{ 59 | Cluster: cb.value, 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/client/plugin.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb" 5 | "github.com/fagongzi/gateway/pkg/pb/metapb" 6 | "github.com/fagongzi/gateway/pkg/pb/rpcpb" 7 | ) 8 | 9 | // PluginBuilder plugin builder 10 | type PluginBuilder struct { 11 | c *client 12 | value metapb.Plugin 13 | } 14 | 15 | // NewPluginBuilder return a plugin build 16 | func (c *client) NewPluginBuilder() *PluginBuilder { 17 | return &PluginBuilder{ 18 | c: c, 19 | value: metapb.Plugin{ 20 | Type: metapb.JavaScript, 21 | }, 22 | } 23 | } 24 | 25 | // Use use a plugin 26 | func (sb *PluginBuilder) Use(value metapb.Plugin) *PluginBuilder { 27 | sb.value = value 28 | return sb 29 | } 30 | 31 | // Name set plugin name 32 | func (sb *PluginBuilder) Name(name string) *PluginBuilder { 33 | sb.value.Name = name 34 | return sb 35 | } 36 | 37 | // Version set plugin version 38 | func (sb *PluginBuilder) Version(version int64) *PluginBuilder { 39 | sb.value.Version = version 40 | return sb 41 | } 42 | 43 | // Author set plugin author 44 | func (sb *PluginBuilder) Author(author, email string) *PluginBuilder { 45 | sb.value.Author = author 46 | sb.value.Email = email 47 | return sb 48 | } 49 | 50 | // Script set plugin script 51 | func (sb *PluginBuilder) Script(content, cfg []byte) *PluginBuilder { 52 | sb.value.Content = content 53 | sb.value.Cfg = cfg 54 | return sb 55 | } 56 | 57 | // Commit commit 58 | func (sb *PluginBuilder) Commit() (uint64, error) { 59 | err := pb.ValidatePlugin(&sb.value) 60 | if err != nil { 61 | return 0, err 62 | } 63 | 64 | return sb.c.putPlugin(sb.value) 65 | } 66 | 67 | // Build build 68 | func (sb *PluginBuilder) Build() (*rpcpb.PutPluginReq, error) { 69 | err := pb.ValidatePlugin(&sb.value) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return &rpcpb.PutPluginReq{ 75 | Plugin: sb.value, 76 | }, nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/client/routing.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb" 5 | "github.com/fagongzi/gateway/pkg/pb/metapb" 6 | "github.com/fagongzi/gateway/pkg/pb/rpcpb" 7 | ) 8 | 9 | // RoutingBuilder routing builder 10 | type RoutingBuilder struct { 11 | c *client 12 | value metapb.Routing 13 | } 14 | 15 | // NewRoutingBuilder return a routing build 16 | func (c *client) NewRoutingBuilder() *RoutingBuilder { 17 | return &RoutingBuilder{ 18 | c: c, 19 | value: metapb.Routing{}, 20 | } 21 | } 22 | 23 | // Use use a cluster 24 | func (rb *RoutingBuilder) Use(value metapb.Routing) *RoutingBuilder { 25 | rb.value = value 26 | return rb 27 | } 28 | 29 | // To routing to 30 | func (rb *RoutingBuilder) To(clusterID uint64) *RoutingBuilder { 31 | rb.value.ClusterID = clusterID 32 | return rb 33 | } 34 | 35 | // AddCondition add condition 36 | func (rb *RoutingBuilder) AddCondition(param metapb.Parameter, op metapb.CMP, expect string) *RoutingBuilder { 37 | rb.value.Conditions = append(rb.value.Conditions, metapb.Condition{ 38 | Parameter: param, 39 | Cmp: op, 40 | Expect: expect, 41 | }) 42 | return rb 43 | } 44 | 45 | // TrafficRate set traffic rate for this routing 46 | func (rb *RoutingBuilder) TrafficRate(rate int) *RoutingBuilder { 47 | rb.value.TrafficRate = int32(rate) 48 | return rb 49 | } 50 | 51 | // Strategy set strategy for this routing 52 | func (rb *RoutingBuilder) Strategy(strategy metapb.RoutingStrategy) *RoutingBuilder { 53 | rb.value.Strategy = strategy 54 | return rb 55 | } 56 | 57 | // Up up this routing 58 | func (rb *RoutingBuilder) Up() *RoutingBuilder { 59 | rb.value.Status = metapb.Up 60 | return rb 61 | } 62 | 63 | // Down down this routing 64 | func (rb *RoutingBuilder) Down() *RoutingBuilder { 65 | rb.value.Status = metapb.Down 66 | return rb 67 | } 68 | 69 | // Name routing name 70 | func (rb *RoutingBuilder) Name(name string) *RoutingBuilder { 71 | rb.value.Name = name 72 | return rb 73 | } 74 | 75 | // API set routing API 76 | func (rb *RoutingBuilder) API(api uint64) *RoutingBuilder { 77 | rb.value.API = api 78 | return rb 79 | } 80 | 81 | // Commit commit 82 | func (rb *RoutingBuilder) Commit() (uint64, error) { 83 | err := pb.ValidateRouting(&rb.value) 84 | if err != nil { 85 | return 0, err 86 | } 87 | 88 | return rb.c.putRouting(rb.value) 89 | } 90 | 91 | // Build build 92 | func (rb *RoutingBuilder) Build() (*rpcpb.PutRoutingReq, error) { 93 | err := pb.ValidateRouting(&rb.value) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return &rpcpb.PutRoutingReq{ 99 | Routing: rb.value, 100 | }, nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/client/server.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/fagongzi/gateway/pkg/pb" 7 | "github.com/fagongzi/gateway/pkg/pb/metapb" 8 | "github.com/fagongzi/gateway/pkg/pb/rpcpb" 9 | ) 10 | 11 | // ServerBuilder server builder 12 | type ServerBuilder struct { 13 | c *client 14 | value metapb.Server 15 | } 16 | 17 | // NewServerBuilder return a server build 18 | func (c *client) NewServerBuilder() *ServerBuilder { 19 | return &ServerBuilder{ 20 | c: c, 21 | value: metapb.Server{}, 22 | } 23 | } 24 | 25 | // Use use a server 26 | func (sb *ServerBuilder) Use(value metapb.Server) *ServerBuilder { 27 | sb.value = value 28 | return sb 29 | } 30 | 31 | // NoHeathCheck no heath check 32 | func (sb *ServerBuilder) NoHeathCheck() *ServerBuilder { 33 | sb.value.HeathCheck = nil 34 | return sb 35 | } 36 | 37 | // CheckHTTPCode use a heath check 38 | func (sb *ServerBuilder) CheckHTTPCode(path string, interval time.Duration, timeout time.Duration) *ServerBuilder { 39 | if sb.value.HeathCheck == nil { 40 | sb.value.HeathCheck = &metapb.HeathCheck{} 41 | 42 | } 43 | 44 | sb.value.HeathCheck.Path = path 45 | sb.value.HeathCheck.Body = "" 46 | sb.value.HeathCheck.CheckInterval = int64(interval) 47 | sb.value.HeathCheck.Timeout = int64(timeout) 48 | return sb 49 | } 50 | 51 | // CheckHTTPBody use a heath check 52 | func (sb *ServerBuilder) CheckHTTPBody(path, body string, interval time.Duration, timeout time.Duration) *ServerBuilder { 53 | if sb.value.HeathCheck == nil { 54 | sb.value.HeathCheck = &metapb.HeathCheck{} 55 | 56 | } 57 | 58 | sb.value.HeathCheck.Path = path 59 | sb.value.HeathCheck.Body = body 60 | sb.value.HeathCheck.CheckInterval = int64(interval) 61 | sb.value.HeathCheck.Timeout = int64(timeout) 62 | return sb 63 | } 64 | 65 | // Addr set addr 66 | func (sb *ServerBuilder) Addr(addr string) *ServerBuilder { 67 | sb.value.Addr = addr 68 | return sb 69 | } 70 | 71 | // HTTPBackend set backend is http backend 72 | func (sb *ServerBuilder) HTTPBackend() *ServerBuilder { 73 | sb.value.Protocol = metapb.HTTP 74 | return sb 75 | } 76 | 77 | // MaxQPS set max qps 78 | func (sb *ServerBuilder) MaxQPS(max int64) *ServerBuilder { 79 | sb.value.MaxQPS = max 80 | return sb 81 | } 82 | 83 | // Weight set robin weight 84 | func (sb *ServerBuilder) Weight(weight int64) *ServerBuilder { 85 | sb.value.Weight = weight 86 | return sb 87 | } 88 | 89 | // NoCircuitBreaker no circuit breaker 90 | func (sb *ServerBuilder) NoCircuitBreaker() *ServerBuilder { 91 | sb.value.CircuitBreaker = nil 92 | return sb 93 | } 94 | 95 | // CircuitBreakerCheckPeriod set circuit breaker period 96 | func (sb *ServerBuilder) CircuitBreakerCheckPeriod(checkPeriod time.Duration) *ServerBuilder { 97 | if sb.value.CircuitBreaker == nil { 98 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{} 99 | } 100 | 101 | sb.value.CircuitBreaker.RateCheckPeriod = int64(checkPeriod) 102 | return sb 103 | } 104 | 105 | // CircuitBreakerHalfTrafficRate set circuit breaker traffic in half status 106 | func (sb *ServerBuilder) CircuitBreakerHalfTrafficRate(rate int) *ServerBuilder { 107 | if sb.value.CircuitBreaker == nil { 108 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{} 109 | } 110 | 111 | sb.value.CircuitBreaker.HalfTrafficRate = int32(rate) 112 | return sb 113 | } 114 | 115 | // CircuitBreakerCloseToHalfTimeout set circuit breaker timeout that close status convert to half 116 | func (sb *ServerBuilder) CircuitBreakerCloseToHalfTimeout(timeout time.Duration) *ServerBuilder { 117 | if sb.value.CircuitBreaker == nil { 118 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{} 119 | } 120 | 121 | sb.value.CircuitBreaker.CloseTimeout = int64(timeout) 122 | return sb 123 | } 124 | 125 | // CircuitBreakerHalfToCloseCondition set circuit breaker condition of half convert to close 126 | func (sb *ServerBuilder) CircuitBreakerHalfToCloseCondition(failureRate int) *ServerBuilder { 127 | if sb.value.CircuitBreaker == nil { 128 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{} 129 | } 130 | 131 | sb.value.CircuitBreaker.FailureRateToClose = int32(failureRate) 132 | return sb 133 | } 134 | 135 | // CircuitBreakerHalfToOpenCondition set circuit breaker condition of half convert to open 136 | func (sb *ServerBuilder) CircuitBreakerHalfToOpenCondition(succeedRate int) *ServerBuilder { 137 | if sb.value.CircuitBreaker == nil { 138 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{} 139 | } 140 | 141 | sb.value.CircuitBreaker.SucceedRateToOpen = int32(succeedRate) 142 | return sb 143 | } 144 | 145 | // Commit commit 146 | func (sb *ServerBuilder) Commit() (uint64, error) { 147 | err := pb.ValidateServer(&sb.value) 148 | if err != nil { 149 | return 0, err 150 | } 151 | 152 | return sb.c.putServer(sb.value) 153 | } 154 | 155 | // Build build 156 | func (sb *ServerBuilder) Build() (*rpcpb.PutServerReq, error) { 157 | err := pb.ValidateServer(&sb.value) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | return &rpcpb.PutServerReq{ 163 | Server: sb.value, 164 | }, nil 165 | } 166 | -------------------------------------------------------------------------------- /pkg/expr/expr_test.go: -------------------------------------------------------------------------------- 1 | package expr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | func TestParse(t *testing.T) { 10 | value := []byte("abc$(") 11 | _, err := Parse(value) 12 | if err == nil { 13 | t.Errorf("expect syntax error: %+v", err) 14 | } 15 | 16 | value = []byte("abc$(abc)") 17 | _, err = Parse(value) 18 | if err == nil { 19 | t.Errorf("expect syntax error: %+v", err) 20 | } 21 | 22 | value = []byte("abc$(origin.)") 23 | _, err = Parse(value) 24 | if err == nil { 25 | t.Errorf("expect syntax error: %+v", err) 26 | } 27 | 28 | value = []byte("abc$(origin.path)(") 29 | _, err = Parse(value) 30 | if err == nil { 31 | t.Errorf("expect syntax error: %+v", err) 32 | } 33 | 34 | value = []byte("abc$(origin.path))") 35 | _, err = Parse(value) 36 | if err == nil { 37 | t.Errorf("expect syntax error: %+v", err) 38 | } 39 | 40 | value = []byte("abc") 41 | exprs, err := Parse(value) 42 | if err != nil { 43 | t.Errorf("parse const error: %+v", err) 44 | } 45 | if len(exprs) != 1 || exprs[0].Name() != "const-expr" { 46 | t.Errorf("parse const error") 47 | } 48 | 49 | value = []byte("$(origin.path)") 50 | exprs, err = Parse(value) 51 | if err != nil { 52 | t.Errorf("parse origin-path error: %+v", err) 53 | } 54 | if len(exprs) != 1 || exprs[0].Name() != "origin-path-expr" { 55 | t.Errorf("parse origin-path error, %+v", exprs) 56 | } 57 | 58 | value = []byte("$(origin.query)") 59 | exprs, err = Parse(value) 60 | if err != nil { 61 | t.Errorf("parse origin-query error: %+v", err) 62 | } 63 | if len(exprs) != 1 || exprs[0].Name() != "origin-query-expr" { 64 | t.Errorf("parse origin-query error, %+v", exprs) 65 | } 66 | 67 | value = []byte("$(origin.query.abc)") 68 | exprs, err = Parse(value) 69 | if err != nil { 70 | t.Errorf("parse origin-query-param error: %+v", err) 71 | } 72 | if len(exprs) != 1 || exprs[0].Name() != "origin-query-param-expr" { 73 | t.Errorf("parse origin-query-param error, %+v", exprs) 74 | } 75 | 76 | value = []byte("$(origin.cookie.abc)") 77 | exprs, err = Parse(value) 78 | if err != nil { 79 | t.Errorf("parse origin-cookie error: %+v", err) 80 | } 81 | if len(exprs) != 1 || exprs[0].Name() != "origin-cookie-expr" { 82 | t.Errorf("parse origin-cookie error, %+v", exprs) 83 | } 84 | 85 | value = []byte("$(origin.header.abc)") 86 | exprs, err = Parse(value) 87 | if err != nil { 88 | t.Errorf("parse origin-header error: %+v", err) 89 | } 90 | if len(exprs) != 1 || exprs[0].Name() != "origin-header-expr" { 91 | t.Errorf("parse origin-header error, %+v", exprs) 92 | } 93 | 94 | value = []byte("$(origin.body.abc.abc.abc)") 95 | exprs, err = Parse(value) 96 | if err != nil { 97 | t.Errorf("parse origin-body error: %+v", err) 98 | } 99 | if len(exprs) != 1 || exprs[0].Name() != "origin-body-expr" { 100 | t.Errorf("parse origin-body error, %+v", exprs) 101 | } 102 | 103 | value = []byte("$(depend.abc.abc.abc)") 104 | exprs, err = Parse(value) 105 | if err != nil { 106 | t.Errorf("parse depend error: %+v", err) 107 | } 108 | if len(exprs) != 1 || exprs[0].Name() != "depend-expr" { 109 | t.Errorf("parse depend error, %+v", exprs) 110 | } 111 | 112 | value = []byte("$(param.abc)") 113 | exprs, err = Parse(value) 114 | if err != nil { 115 | t.Errorf("parse param error: %+v", err) 116 | } 117 | if len(exprs) != 1 || exprs[0].Name() != "param-expr" { 118 | t.Errorf("parse param error, %+v", exprs) 119 | } 120 | 121 | value = []byte("/$(origin.path)?id=$(param.abc)&value=$(origin.body.abc.abc)&value2=$(depend.abc.abc.abc)&value3=$(origin.header.abc)&value4=4") 122 | exprs, err = Parse(value) 123 | if err != nil { 124 | t.Errorf("parse param error: %+v", err) 125 | } 126 | if len(exprs) != 11 { 127 | t.Errorf("expect 11 expers but %d", len(exprs)) 128 | } 129 | } 130 | 131 | func TestExec(t *testing.T) { 132 | exprs, err := Parse([]byte("$(origin.query.names)")) 133 | if err != nil { 134 | t.Errorf("expect syntax error: %+v", err) 135 | } 136 | 137 | req := fasthttp.AcquireRequest() 138 | req.SetRequestURI("http://127.0.0.1/path?names=abc&names=abc2") 139 | value := Exec(&Ctx{ 140 | Origin: req, 141 | }, exprs...) 142 | 143 | if string(value) != "abc" { 144 | t.Errorf("expect but %s", value) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pkg/filter/cache_util.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/fagongzi/goetty" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | // NewCachedValue returns a cached value 9 | func NewCachedValue(resp *fasthttp.Response) *goetty.ByteBuf { 10 | buf := goetty.NewByteBuf(128) 11 | buf.WriteInt(0) 12 | n := 0 13 | resp.Header.VisitAll(func(key, value []byte) { 14 | buf.WriteInt(len(key)) 15 | buf.Write(key) 16 | buf.WriteInt(len(value)) 17 | buf.Write(value) 18 | n++ 19 | }) 20 | buf.WriteInt(len(resp.Body())) 21 | buf.Write(resp.Body()) 22 | 23 | goetty.Int2BytesTo(n, buf.RawBuf()) 24 | return buf 25 | } 26 | 27 | // ReadCachedValueTo read cached value to response 28 | func ReadCachedValueTo(buf *goetty.ByteBuf, resp *fasthttp.Response) { 29 | headers, _ := buf.ReadInt() 30 | for i := 0; i < headers; i++ { 31 | resp.Header.SetBytesKV(readBytes(buf), readBytes(buf)) 32 | } 33 | 34 | resp.SetBody(readBytes(buf)) 35 | } 36 | 37 | func readBytes(buf *goetty.ByteBuf) []byte { 38 | n, _ := buf.ReadInt() 39 | _, value, _ := buf.ReadBytes(n) 40 | return value 41 | } 42 | -------------------------------------------------------------------------------- /pkg/filter/const.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | const ( 4 | // AttrClientRealIP real client ip 5 | AttrClientRealIP = "__internal_real_ip__" 6 | // AttrUsingCachingValue using cached value to response 7 | AttrUsingCachingValue = "__internal_using_cache_value__" 8 | // AttrUsingResponse using response to response 9 | AttrUsingResponse = "__internal_using_response__" 10 | 11 | // BreakFilterChainCode break filter chain code 12 | BreakFilterChainCode = -1 13 | ) 14 | 15 | // StringValue returns the attr value 16 | func StringValue(attr string, c Context) string { 17 | return c.GetAttr(attr).(string) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/fagongzi/gateway/pkg/pb/metapb" 7 | "github.com/fagongzi/gateway/pkg/util" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | // Context filter context 12 | type Context interface { 13 | StartAt() time.Time 14 | EndAt() time.Time 15 | 16 | OriginRequest() *fasthttp.RequestCtx 17 | ForwardRequest() *fasthttp.Request 18 | Response() *fasthttp.Response 19 | 20 | API() *metapb.API 21 | DispatchNode() *metapb.DispatchNode 22 | Server() *metapb.Server 23 | Analysis() *util.Analysis 24 | 25 | SetAttr(key string, value interface{}) 26 | GetAttr(key string) interface{} 27 | } 28 | 29 | // Filter filter interface 30 | type Filter interface { 31 | Name() string 32 | Init(cfg string) error 33 | 34 | Pre(c Context) (statusCode int, err error) 35 | Post(c Context) (statusCode int, err error) 36 | PostErr(c Context, code int, err error) 37 | } 38 | 39 | // BaseFilter base filter support default implemention 40 | type BaseFilter struct{} 41 | 42 | // Init init filter 43 | func (f BaseFilter) Init(cfg string) error { 44 | return nil 45 | } 46 | 47 | // Pre execute before proxy 48 | func (f BaseFilter) Pre(c Context) (statusCode int, err error) { 49 | return fasthttp.StatusOK, nil 50 | } 51 | 52 | // Post execute after proxy 53 | func (f BaseFilter) Post(c Context) (statusCode int, err error) { 54 | return fasthttp.StatusOK, nil 55 | } 56 | 57 | // PostErr execute proxy has errors 58 | func (f BaseFilter) PostErr(c Context, code int, err error) { 59 | 60 | } 61 | -------------------------------------------------------------------------------- /pkg/filter/test_help.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/fagongzi/gateway/pkg/pb/metapb" 8 | "github.com/fagongzi/gateway/pkg/util" 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | // TestContext the context for test 13 | type TestContext struct { 14 | Attr sync.Map 15 | StartAtValue, EndAtValue time.Time 16 | OriginValue *fasthttp.RequestCtx 17 | ForwardValue *fasthttp.Request 18 | ResponseValue *fasthttp.Response 19 | APIValue *metapb.API 20 | NodeValue *metapb.DispatchNode 21 | ServerValue *metapb.Server 22 | AnalysisValue *util.Analysis 23 | } 24 | 25 | // StartAt returns StartAt value 26 | func (ctx *TestContext) StartAt() time.Time { 27 | return ctx.StartAtValue 28 | } 29 | 30 | // EndAt returns EndAt value 31 | func (ctx *TestContext) EndAt() time.Time { 32 | return ctx.EndAtValue 33 | } 34 | 35 | // OriginRequest returns OriginRequest value 36 | func (ctx *TestContext) OriginRequest() *fasthttp.RequestCtx { 37 | return ctx.OriginValue 38 | } 39 | 40 | // ForwardRequest returns ForwardRequest value 41 | func (ctx *TestContext) ForwardRequest() *fasthttp.Request { 42 | return ctx.ForwardValue 43 | } 44 | 45 | // Response returns Response value 46 | func (ctx *TestContext) Response() *fasthttp.Response { 47 | return ctx.ResponseValue 48 | } 49 | 50 | // API returns API value 51 | func (ctx *TestContext) API() *metapb.API { 52 | return ctx.APIValue 53 | } 54 | 55 | // DispatchNode returns DispatchNode value 56 | func (ctx *TestContext) DispatchNode() *metapb.DispatchNode { 57 | return ctx.NodeValue 58 | } 59 | 60 | // Server returns Server value 61 | func (ctx *TestContext) Server() *metapb.Server { 62 | return ctx.ServerValue 63 | } 64 | 65 | // Analysis returns Analysis value 66 | func (ctx *TestContext) Analysis() *util.Analysis { 67 | return ctx.AnalysisValue 68 | } 69 | 70 | // SetAttr set attr 71 | func (ctx *TestContext) SetAttr(key string, value interface{}) { 72 | ctx.Attr.Store(key, value) 73 | } 74 | 75 | // GetAttr get attr value 76 | func (ctx *TestContext) GetAttr(key string) interface{} { 77 | value, _ := ctx.Attr.Load(key) 78 | return value 79 | } 80 | -------------------------------------------------------------------------------- /pkg/lb/haship.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "hash/fnv" 5 | 6 | "github.com/valyala/fasthttp" 7 | 8 | "github.com/fagongzi/gateway/pkg/pb/metapb" 9 | "github.com/fagongzi/gateway/pkg/util" 10 | ) 11 | 12 | // HashIPBalance is hash IP loadBalance impl 13 | type HashIPBalance struct { 14 | } 15 | 16 | // NewHashIPBalance create a HashIPBalance 17 | func NewHashIPBalance() LoadBalance { 18 | lb := HashIPBalance{} 19 | return lb 20 | } 21 | 22 | // Select select a server from servers using HashIPBalance 23 | func (haship HashIPBalance) Select(ctx *fasthttp.RequestCtx, servers []metapb.Server) uint64 { 24 | l := len(servers) 25 | if 0 >= l { 26 | return 0 27 | } 28 | hash := fnv.New32a() 29 | // key is client ip 30 | key := util.ClientIP(ctx) 31 | hash.Write([]byte(key)) 32 | serve := servers[hash.Sum32()%uint32(l)] 33 | return serve.ID 34 | } 35 | -------------------------------------------------------------------------------- /pkg/lb/haship_test.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fagongzi/gateway/pkg/pb/metapb" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | var ( 11 | Servers = []metapb.Server{ 12 | metapb.Server{ 13 | ID: 1, 14 | Weight: 10, 15 | }, 16 | metapb.Server{ 17 | ID: 2, 18 | Weight: 20, 19 | }, 20 | metapb.Server{ 21 | ID: 3, 22 | Weight: 40, 23 | }, 24 | metapb.Server{ 25 | ID: 5, 26 | Weight: 50, 27 | }, 28 | metapb.Server{ 29 | ID: 19, 30 | Weight: 20, 31 | }, 32 | } 33 | ) 34 | 35 | func Test_HashIPBalance(t *testing.T) { 36 | lb := NewHashIPBalance() 37 | reqCtx := &fasthttp.RequestCtx{} 38 | reqCtx.Request.Header.Add("X-Forwarded-For", "192.168.0.5") 39 | for i := 0; i < 66; i++ { 40 | id := lb.Select(reqCtx, Servers) 41 | if id < 1 { 42 | t.Errorf("Test_HashIPBalance is error=%d", id) 43 | } 44 | t.Logf("id=%d", id) 45 | } 46 | } 47 | 48 | func Benchmark_HashIPBalance(b *testing.B) { 49 | lb := NewHashIPBalance() 50 | reqCtx := &fasthttp.RequestCtx{} 51 | reqCtx.Request.Header.Add("X-Forwarded-For", "192.168.0.5") 52 | for i := 0; i < b.N; i++ { 53 | id := lb.Select(reqCtx, Servers) 54 | if id < 1 { 55 | b.Errorf("Test_HashIPBalance is error=%d", id) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/lb/lb.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | var ( 9 | supportLbs = []metapb.LoadBalance{metapb.RoundRobin} 10 | ) 11 | 12 | var ( 13 | // LBS map loadBalance name and process function 14 | LBS = map[metapb.LoadBalance]func() LoadBalance{ 15 | metapb.RoundRobin: NewRoundRobin, 16 | metapb.WightRobin: NewWeightRobin, 17 | metapb.IPHash: NewHashIPBalance, 18 | metapb.Rand: NewRandBalance, 19 | } 20 | ) 21 | 22 | // LoadBalance loadBalance interface returns selected server's id 23 | type LoadBalance interface { 24 | Select(ctx *fasthttp.RequestCtx, servers []metapb.Server) uint64 25 | } 26 | 27 | // GetSupportLBS return supported loadBalances 28 | func GetSupportLBS() []metapb.LoadBalance { 29 | return supportLbs 30 | } 31 | 32 | // NewLoadBalance create a LoadBalance,if LoadBalance function is not supported 33 | // it will return NewRoundRobin 34 | func NewLoadBalance(name metapb.LoadBalance) LoadBalance { 35 | if l, ok := LBS[name]; ok { 36 | return l() 37 | } 38 | return NewRoundRobin() 39 | } 40 | -------------------------------------------------------------------------------- /pkg/lb/rand.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "github.com/valyala/fasthttp" 5 | "github.com/valyala/fastrand" 6 | 7 | "github.com/fagongzi/gateway/pkg/pb/metapb" 8 | ) 9 | 10 | // RandBalance is rand loadBalance impl 11 | type RandBalance struct { 12 | } 13 | 14 | // NewRandBalance create a RandBalance 15 | func NewRandBalance() LoadBalance { 16 | lb := RandBalance{} 17 | return lb 18 | } 19 | 20 | // Select select a server from servers using fastrand 21 | func (rb RandBalance) Select(ctx *fasthttp.RequestCtx, servers []metapb.Server) uint64 { 22 | l := len(servers) 23 | if 0 >= l { 24 | return 0 25 | } 26 | server := servers[fastrand.Uint32n(uint32(l))] 27 | return server.ID 28 | } 29 | -------------------------------------------------------------------------------- /pkg/lb/rand_test.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "github.com/valyala/fasthttp" 5 | "testing" 6 | ) 7 | 8 | func Test_RandBalance(t *testing.T) { 9 | lb := NewRandBalance() 10 | reqCtx := &fasthttp.RequestCtx{} 11 | for i := 0; i < 66; i++ { 12 | id := lb.Select(reqCtx, Servers) 13 | if id < 1 { 14 | t.Errorf("Test_HashIPBalance is error=%d", id) 15 | } 16 | t.Logf("id=%d", id) 17 | } 18 | } 19 | 20 | func Benchmark_RandBalance(b *testing.B) { 21 | lb := NewRandBalance() 22 | reqCtx := &fasthttp.RequestCtx{} 23 | for i := 0; i < b.N; i++ { 24 | id := lb.Select(reqCtx, Servers) 25 | if id < 1 { 26 | b.Errorf("Test_HashIPBalance is error=%d", id) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/lb/roundrobin.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/fagongzi/gateway/pkg/pb/metapb" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | // RoundRobin round robin loadBalance impl 11 | type RoundRobin struct { 12 | ops *uint64 13 | } 14 | 15 | // NewRoundRobin create a RoundRobin 16 | func NewRoundRobin() LoadBalance { 17 | var ops uint64 18 | ops = 0 19 | 20 | return RoundRobin{ 21 | ops: &ops, 22 | } 23 | } 24 | 25 | // Select select a server from servers using RoundRobin 26 | func (rr RoundRobin) Select(req *fasthttp.RequestCtx, servers []metapb.Server) uint64 { 27 | l := uint64(len(servers)) 28 | 29 | if 0 >= l { 30 | return 0 31 | } 32 | 33 | target := servers[int(atomic.AddUint64(rr.ops, 1)%l)] 34 | return target.ID 35 | } 36 | -------------------------------------------------------------------------------- /pkg/lb/weightrobin.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | // WeightRobin weight robin loadBalance impl 9 | type WeightRobin struct { 10 | opts map[uint64]*weightRobin 11 | } 12 | 13 | // weightRobin used to save the weight info of server 14 | type weightRobin struct { 15 | effectiveWeight int64 16 | currentWeight int64 17 | } 18 | 19 | // NewWeightRobin create a WeightRobin 20 | func NewWeightRobin() LoadBalance { 21 | return &WeightRobin{ 22 | opts: make(map[uint64]*weightRobin, 1024), 23 | } 24 | } 25 | 26 | // Select select a server from servers using WeightRobin 27 | func (w *WeightRobin) Select(req *fasthttp.RequestCtx, servers []metapb.Server) (best uint64) { 28 | var total int64 29 | l := len(servers) 30 | if 0 >= l { 31 | return 0 32 | } 33 | 34 | for i := l - 1; i >= 0; i-- { 35 | svr := servers[i] 36 | 37 | id := svr.ID 38 | if _, ok := w.opts[id]; !ok { 39 | w.opts[id] = &weightRobin{ 40 | effectiveWeight: svr.Weight, 41 | } 42 | } 43 | 44 | wt := w.opts[id] 45 | wt.currentWeight += wt.effectiveWeight 46 | total += wt.effectiveWeight 47 | 48 | if wt.effectiveWeight < svr.Weight { 49 | wt.effectiveWeight++ 50 | } 51 | 52 | if best == 0 || w.opts[uint64(best)] == nil || wt.currentWeight > w.opts[best].currentWeight { 53 | best = id 54 | } 55 | } 56 | 57 | if best == 0 { 58 | return 0 59 | } 60 | 61 | w.opts[best].currentWeight -= total 62 | 63 | return best 64 | } 65 | -------------------------------------------------------------------------------- /pkg/lb/weightrobin_test.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fagongzi/gateway/pkg/pb/metapb" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | func TestWeightRobin_Select(t *testing.T) { 11 | var values []metapb.Server 12 | values = append(values, metapb.Server{ 13 | ID: 1, 14 | Weight: 20, 15 | }) 16 | 17 | values = append(values, metapb.Server{ 18 | ID: 2, 19 | Weight: 10, 20 | }) 21 | 22 | values = append(values, metapb.Server{ 23 | ID: 3, 24 | Weight: 35, 25 | }) 26 | 27 | values = append(values, metapb.Server{ 28 | ID: 4, 29 | Weight: 5, 30 | }) 31 | 32 | type fields struct { 33 | opts map[uint64]*weightRobin 34 | } 35 | type args struct { 36 | req *fasthttp.RequestCtx 37 | servers []metapb.Server 38 | } 39 | tests := []struct { 40 | name string 41 | fields fields 42 | args args 43 | wantBest []int 44 | }{ 45 | { 46 | name: "test_case_1", 47 | fields: struct{ opts map[uint64]*weightRobin }{opts: make(map[uint64]*weightRobin, 50)}, 48 | args: struct { 49 | req *fasthttp.RequestCtx 50 | servers []metapb.Server 51 | }{req: nil, servers: values}, 52 | wantBest: []int{20, 10, 35, 5}, 53 | }, 54 | } 55 | for _, tt := range tests { 56 | var res = make(map[uint64]int) 57 | t.Run(tt.name, func(t *testing.T) { 58 | w := &WeightRobin{ 59 | opts: tt.fields.opts, 60 | } 61 | for i := 0; i < 70; i++ { 62 | res[w.Select(tt.args.req, tt.args.servers)]++ 63 | } 64 | }) 65 | for k, v := range res { 66 | if tt.wantBest[k-1] != v { 67 | t.Errorf("WeightRobin.Select() = %v, want %v", res, tt.wantBest) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/pb/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Generate all gateway protobuf bindings. 4 | # Run from repository root. 5 | # 6 | set -e 7 | 8 | # directories containing protos to be built 9 | DIRS="./metapb ./rpcpb" 10 | 11 | GOGOPROTO_ROOT="${GOPATH}/src/github.com/gogo/protobuf" 12 | GOGOPROTO_PATH="${GOGOPROTO_ROOT}:${GOGOPROTO_ROOT}/protobuf" 13 | 14 | GATEWAY_PB_PATH="${GOPATH}/src/github.com/fagongzi/gateway/pkg/pb" 15 | 16 | 17 | for dir in ${DIRS}; do 18 | pushd ${dir} 19 | protoc --gofast_out=plugins=grpc,import_prefix=github.com/fagongzi/gateway/pkg/pb/:. -I=.:"${GOGOPROTO_PATH}":"${GATEWAY_PB_PATH}":"${GOPATH}/src" *.proto 20 | sed -i.bak -E "s/github\.com\/fagongzi\/gateway\/pkg\/pb\/(gogoproto|github\.com|golang\.org|google\.golang\.org)/\1/g" *.pb.go 21 | sed -i.bak -E 's/github\.com\/fagongzi\/gateway\/pkg\/pb\/(errors|fmt|io)/\1/g' *.pb.go 22 | sed -i.bak -E 's/import _ \"gogoproto\"//g' *.pb.go 23 | sed -i.bak -E 's/import fmt \"fmt\"//g' *.pb.go 24 | sed -i.bak -E 's/import math \"github.com\/fagongzi\/gateway\/pkg\/pb\/math\"//g' *.pb.go 25 | rm -f *.bak 26 | goimports -w *.pb.go 27 | popd 28 | done 29 | -------------------------------------------------------------------------------- /pkg/pb/rpcpb/services.go: -------------------------------------------------------------------------------- 1 | package rpcpb 2 | 3 | const ( 4 | // ServiceMeta meta service name 5 | ServiceMeta = "gateway-service-meta" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/pb/validation.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/fagongzi/gateway/pkg/expr" 8 | "github.com/fagongzi/gateway/pkg/pb/metapb" 9 | "github.com/fagongzi/gateway/pkg/plugin" 10 | ) 11 | 12 | // ValidateRouting validate routing 13 | func ValidateRouting(value *metapb.Routing) error { 14 | if value.API == 0 { 15 | return fmt.Errorf("missing api") 16 | } 17 | 18 | if value.ClusterID == 0 { 19 | return fmt.Errorf("missing cluster") 20 | } 21 | 22 | if value.Name == "" { 23 | return fmt.Errorf("missing name") 24 | } 25 | 26 | if value.TrafficRate <= 0 || value.TrafficRate > 100 { 27 | return fmt.Errorf("error traffic rate: %d", value.TrafficRate) 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // ValidateCluster validate cluster 34 | func ValidateCluster(value *metapb.Cluster) error { 35 | if value.Name == "" { 36 | return fmt.Errorf("missing name") 37 | } 38 | 39 | return nil 40 | } 41 | 42 | // ValidateServer validate server 43 | func ValidateServer(value *metapb.Server) error { 44 | if value.Addr == "" { 45 | return fmt.Errorf("missing server address") 46 | } 47 | 48 | if value.MaxQPS == 0 { 49 | return fmt.Errorf("missing server max qps") 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // ValidateAPI validate api 56 | func ValidateAPI(value *metapb.API) error { 57 | if value.Name == "" { 58 | return fmt.Errorf("missing api name") 59 | } 60 | 61 | if value.URLPattern == "" { 62 | return fmt.Errorf("missing URLPattern") 63 | } 64 | 65 | for _, n := range value.Nodes { 66 | if n.URLRewrite != "" { 67 | _, err := expr.Parse([]byte(n.URLRewrite)) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | 73 | for _, v := range n.Validations { 74 | for _, r := range v.Rules { 75 | if r.RuleType == metapb.RuleRegexp { 76 | _, err := regexp.Compile(r.Expression) 77 | if err != nil { 78 | return err 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // ValidatePlugin validate plugin 89 | func ValidatePlugin(value *metapb.Plugin) error { 90 | if value.Name == "" { 91 | return fmt.Errorf("missing plugin name") 92 | } 93 | 94 | if value.Version == 0 { 95 | return fmt.Errorf("missing plugin version") 96 | } 97 | 98 | if len(value.Content) == 0 { 99 | return fmt.Errorf("missing plugin content") 100 | } 101 | 102 | _, err := plugin.NewRuntime(value) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /pkg/plugin/builtin_base.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | const ( 4 | httpModuleName = "http" 5 | jsonModuleName = "json" 6 | logModuleName = "log" 7 | redisModuleName = "redis" 8 | ) 9 | 10 | var ( 11 | httpModule = newHTTPModule() 12 | jsonModule = &JSONModule{} 13 | logModule = &LogModule{} 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/plugin/builtin_http.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | // HTTPResult result 13 | type HTTPResult struct { 14 | rsp *http.Response 15 | err error 16 | body string 17 | } 18 | 19 | func newHTTPResult(rsp *http.Response, err error) *HTTPResult { 20 | if rsp != nil { 21 | defer rsp.Body.Close() 22 | 23 | data, err := ioutil.ReadAll(rsp.Body) 24 | if err != nil { 25 | return &HTTPResult{ 26 | err: err, 27 | } 28 | } 29 | 30 | return &HTTPResult{ 31 | err: err, 32 | body: string(data), 33 | rsp: rsp, 34 | } 35 | } 36 | 37 | return &HTTPResult{ 38 | err: err, 39 | rsp: rsp, 40 | } 41 | } 42 | 43 | // HasError returns true if has a error 44 | func (res *HTTPResult) HasError() bool { 45 | return res.err != nil 46 | } 47 | 48 | // Error returns error 49 | func (res *HTTPResult) Error() string { 50 | if res.err != nil { 51 | return res.err.Error() 52 | } 53 | 54 | return "" 55 | } 56 | 57 | // StatusCode returns status code 58 | func (res *HTTPResult) StatusCode() int { 59 | if res.HasError() { 60 | return 0 61 | } 62 | 63 | return res.rsp.StatusCode 64 | } 65 | 66 | // Header returns http response header 67 | func (res *HTTPResult) Header() map[string][]string { 68 | headers := make(map[string][]string) 69 | if res.HasError() { 70 | return headers 71 | } 72 | 73 | for key, values := range res.rsp.Header { 74 | headers[key] = values 75 | } 76 | 77 | return headers 78 | } 79 | 80 | // Cookie returns http response cookie 81 | func (res *HTTPResult) Cookie() []*http.Cookie { 82 | if res.HasError() { 83 | return nil 84 | } 85 | 86 | return res.rsp.Cookies() 87 | } 88 | 89 | // Body returns http response body 90 | func (res *HTTPResult) Body() string { 91 | if res.HasError() { 92 | return "" 93 | } 94 | 95 | return res.body 96 | } 97 | 98 | // HTTPModule http module 99 | type HTTPModule struct { 100 | client *http.Client 101 | } 102 | 103 | func newHTTPModule() *HTTPModule { 104 | client := &http.Client{} 105 | *client = *http.DefaultClient 106 | client.Timeout = time.Second * 30 107 | 108 | return &HTTPModule{ 109 | client: client, 110 | } 111 | } 112 | 113 | // NewHTTPResponse returns http response 114 | func (h *HTTPModule) NewHTTPResponse() *FastHTTPResponseAdapter { 115 | return newFastHTTPResponseAdapter(fasthttp.AcquireResponse()) 116 | } 117 | 118 | // Get go get 119 | func (h *HTTPModule) Get(url string) *HTTPResult { 120 | rsp, err := h.client.Get(url) 121 | return newHTTPResult(rsp, err) 122 | } 123 | 124 | // Post do post 125 | func (h *HTTPModule) Post(url string, body string, header map[string][]string) *HTTPResult { 126 | return h.do("POST", url, body, header) 127 | } 128 | 129 | // PostJSON do post 130 | func (h *HTTPModule) PostJSON(url string, body string, header map[string][]string) *HTTPResult { 131 | header["Content-Type"] = []string{"application/json"} 132 | return h.Post(url, body, header) 133 | } 134 | 135 | // Put do put 136 | func (h *HTTPModule) Put(url string, body string, header map[string][]string) *HTTPResult { 137 | return h.do("PUT", url, body, header) 138 | } 139 | 140 | // PutJSON do put json 141 | func (h *HTTPModule) PutJSON(url string, body string, header map[string][]string) *HTTPResult { 142 | header["Content-Type"] = []string{"application/json"} 143 | return h.Put(url, body, header) 144 | } 145 | 146 | // Delete do delete 147 | func (h *HTTPModule) Delete(url string, body string, header map[string][]string) *HTTPResult { 148 | return h.do("DELETE", url, body, header) 149 | } 150 | 151 | // DeleteJSON do delete json 152 | func (h *HTTPModule) DeleteJSON(url string, body string, header map[string][]string) *HTTPResult { 153 | header["Content-Type"] = []string{"application/json"} 154 | return h.Delete(url, body, header) 155 | } 156 | 157 | func (h *HTTPModule) do(method string, url string, body string, header map[string][]string) *HTTPResult { 158 | r := bytes.NewReader([]byte(body)) 159 | req, err := http.NewRequest(method, url, r) 160 | if err != nil { 161 | return newHTTPResult(nil, err) 162 | } 163 | 164 | for key, values := range header { 165 | for _, value := range values { 166 | req.Header.Add(key, value) 167 | } 168 | } 169 | 170 | rsp, err := h.client.Do(req) 171 | return newHTTPResult(rsp, err) 172 | } 173 | -------------------------------------------------------------------------------- /pkg/plugin/builtin_json.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // JSONModule json builtin 8 | type JSONModule struct { 9 | } 10 | 11 | // Stringify returns json string 12 | func (j *JSONModule) Stringify(value interface{}) string { 13 | v, _ := json.Marshal(value) 14 | return string(v) 15 | } 16 | 17 | // Parse parse a string to json 18 | func (j *JSONModule) Parse(value string) map[string]interface{} { 19 | obj := make(map[string]interface{}) 20 | json.Unmarshal([]byte(value), &obj) 21 | return obj 22 | } 23 | -------------------------------------------------------------------------------- /pkg/plugin/builtin_log.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/fagongzi/log" 5 | ) 6 | 7 | // LogModule log module 8 | type LogModule struct { 9 | } 10 | 11 | // Info info 12 | func (l *LogModule) Info(v ...interface{}) { 13 | log.Info(v...) 14 | } 15 | 16 | // Infof infof 17 | func (l *LogModule) Infof(format string, v ...interface{}) { 18 | log.Infof(format, v...) 19 | } 20 | 21 | // Debug debug 22 | func (l *LogModule) Debug(v ...interface{}) { 23 | log.Debug(v...) 24 | } 25 | 26 | // Debugf debugf 27 | func (l *LogModule) Debugf(format string, v ...interface{}) { 28 | log.Debugf(format, v...) 29 | } 30 | 31 | // Warn warn 32 | func (l *LogModule) Warn(v ...interface{}) { 33 | log.Warning(v...) 34 | } 35 | 36 | // Warnf warnf 37 | func (l *LogModule) Warnf(format string, v ...interface{}) { 38 | log.Warningf(format, v...) 39 | } 40 | 41 | // Warning warning 42 | func (l *LogModule) Warning(v ...interface{}) { 43 | log.Warning(v...) 44 | } 45 | 46 | // Warningf warningf 47 | func (l *LogModule) Warningf(format string, v ...interface{}) { 48 | log.Warningf(format, v...) 49 | } 50 | 51 | // Error error 52 | func (l *LogModule) Error(v ...interface{}) { 53 | log.Error(v...) 54 | } 55 | 56 | // Errorf errorf 57 | func (l *LogModule) Errorf(format string, v ...interface{}) { 58 | log.Errorf(format, v...) 59 | } 60 | 61 | // Fatal fatal 62 | func (l *LogModule) Fatal(v ...interface{}) { 63 | log.Fatal(v...) 64 | } 65 | 66 | // Fatalf fatalf 67 | func (l *LogModule) Fatalf(format string, v ...interface{}) { 68 | log.Fatalf(format, v...) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/plugin/builtin_redis.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | ) 8 | 9 | // RedisModule redis module 10 | type RedisModule struct { 11 | rt *Runtime 12 | } 13 | 14 | // CreateRedis create redis 15 | func (m *RedisModule) CreateRedis(cfg map[string]interface{}) *RedisOp { 16 | p := &redis.Pool{ 17 | MaxActive: int(cfg["maxActive"].(int64)), 18 | MaxIdle: int(cfg["maxIdle"].(int64)), 19 | IdleTimeout: time.Second * time.Duration(int(cfg["idleTimeout"].(int64))), 20 | Dial: func() (redis.Conn, error) { 21 | return redis.Dial("tcp", 22 | cfg["addr"].(string), 23 | redis.DialWriteTimeout(time.Second*10)) 24 | }, 25 | } 26 | 27 | m.rt.addCloser(p) 28 | 29 | conn := p.Get() 30 | _, err := conn.Do("PING") 31 | if err != nil { 32 | conn.Close() 33 | return &RedisOp{ 34 | err: err, 35 | } 36 | } 37 | 38 | conn.Close() 39 | return &RedisOp{ 40 | pool: p, 41 | } 42 | } 43 | 44 | // RedisOp redis 45 | type RedisOp struct { 46 | err error 47 | pool *redis.Pool 48 | } 49 | 50 | // Do do redis cmd 51 | func (r *RedisOp) Do(cmd string, args ...interface{}) *CmdResp { 52 | if r.err != nil { 53 | return &CmdResp{ 54 | err: r.err, 55 | } 56 | } 57 | 58 | conn := r.pool.Get() 59 | rsp, err := conn.Do(cmd, args...) 60 | if err != nil { 61 | conn.Close() 62 | return &CmdResp{ 63 | err: err, 64 | } 65 | } 66 | 67 | conn.Close() 68 | return &CmdResp{ 69 | rsp: rsp, 70 | } 71 | } 72 | 73 | // CmdResp redis cmd resp 74 | type CmdResp struct { 75 | err error 76 | rsp interface{} 77 | } 78 | 79 | // HasError returns has error 80 | func (r *CmdResp) HasError() bool { 81 | return r.err != nil 82 | } 83 | 84 | // Error returns error 85 | func (r *CmdResp) Error() string { 86 | if r.err != nil { 87 | return r.err.Error() 88 | } 89 | 90 | return "" 91 | } 92 | 93 | // StringValue returns string value 94 | func (r *CmdResp) StringValue() string { 95 | if r.HasError() { 96 | return "" 97 | } 98 | 99 | value, _ := redis.String(r.rsp, nil) 100 | return value 101 | } 102 | 103 | // StringsValue returns strings value 104 | func (r *CmdResp) StringsValue() []string { 105 | if r.HasError() { 106 | return nil 107 | } 108 | 109 | value, _ := redis.Strings(r.rsp, nil) 110 | return value 111 | } 112 | 113 | // StringMapValue returns string map value 114 | func (r *CmdResp) StringMapValue() map[string]string { 115 | if r.HasError() { 116 | return make(map[string]string) 117 | } 118 | 119 | value, _ := redis.StringMap(r.rsp, nil) 120 | return value 121 | } 122 | 123 | // IntValue returns int value 124 | func (r *CmdResp) IntValue() int { 125 | if r.HasError() { 126 | return 0 127 | } 128 | 129 | value, _ := redis.Int(r.rsp, nil) 130 | return value 131 | } 132 | 133 | // IntsValue returns ints value 134 | func (r *CmdResp) IntsValue() []int { 135 | if r.HasError() { 136 | return nil 137 | } 138 | 139 | value, _ := redis.Ints(r.rsp, nil) 140 | return value 141 | } 142 | 143 | // IntMapValue returns int map value 144 | func (r *CmdResp) IntMapValue() map[string]int { 145 | if r.HasError() { 146 | return make(map[string]int) 147 | } 148 | 149 | value, _ := redis.IntMap(r.rsp, nil) 150 | return value 151 | } 152 | 153 | // Int64Value returns int64 value 154 | func (r *CmdResp) Int64Value() int64 { 155 | if r.HasError() { 156 | return 0 157 | } 158 | 159 | value, _ := redis.Int64(r.rsp, nil) 160 | return value 161 | } 162 | 163 | // Int64sValue returns int64s value 164 | func (r *CmdResp) Int64sValue() []int64 { 165 | if r.HasError() { 166 | return nil 167 | } 168 | 169 | value, _ := redis.Int64s(r.rsp, nil) 170 | return value 171 | } 172 | 173 | // Int64MapValue returns int64 map value 174 | func (r *CmdResp) Int64MapValue() map[string]int64 { 175 | if r.HasError() { 176 | return make(map[string]int64) 177 | } 178 | 179 | value, _ := redis.Int64Map(r.rsp, nil) 180 | return value 181 | } 182 | -------------------------------------------------------------------------------- /pkg/plugin/engine.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/fagongzi/gateway/pkg/filter" 9 | "github.com/fagongzi/gateway/pkg/pb/metapb" 10 | "github.com/fagongzi/log" 11 | ) 12 | 13 | // Engine plugin engine 14 | type Engine struct { 15 | filter.BaseFilter 16 | 17 | name string 18 | enable bool 19 | applied []*Runtime 20 | lastActive time.Time 21 | } 22 | 23 | // NewEngine returns a plugin engine 24 | func NewEngine(enable bool, name string) *Engine { 25 | return &Engine{ 26 | enable: enable, 27 | name: name, 28 | } 29 | } 30 | 31 | // LastActive returns the time that last used 32 | func (eng *Engine) LastActive() time.Time { 33 | return eng.lastActive 34 | } 35 | 36 | // Destroy destory all applied plugins 37 | func (eng *Engine) Destroy() { 38 | for _, rt := range eng.applied { 39 | rt.destroy() 40 | } 41 | } 42 | 43 | // UpdatePlugin update plugin 44 | func (eng *Engine) UpdatePlugin(plugin *metapb.Plugin) error { 45 | target := -1 46 | for idx, rt := range eng.applied { 47 | if rt.meta.ID == plugin.ID { 48 | target = idx 49 | break 50 | } 51 | } 52 | 53 | if target == -1 { 54 | return fmt.Errorf("plugin not found") 55 | } 56 | 57 | rt, err := NewRuntime(plugin) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | eng.applied[target] = rt 63 | return nil 64 | } 65 | 66 | // ApplyPlugins apply plugins 67 | func (eng *Engine) ApplyPlugins(plugins ...*metapb.Plugin) error { 68 | var applied []*Runtime 69 | for idx, plugin := range plugins { 70 | rt, err := NewRuntime(plugin) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | applied = append(applied, rt) 76 | log.Infof("plugin: %d/%s:%d applied with index %d", 77 | plugin.ID, 78 | plugin.Name, 79 | plugin.Version, 80 | idx) 81 | } 82 | 83 | eng.applied = applied 84 | return nil 85 | } 86 | 87 | // Name returns filter name 88 | func (eng *Engine) Name() string { 89 | return eng.name 90 | } 91 | 92 | // Init returns error if init failed 93 | func (eng *Engine) Init(cfg string) error { 94 | return nil 95 | } 96 | 97 | // Pre filter pre method 98 | func (eng *Engine) Pre(c filter.Context) (int, error) { 99 | if !eng.enable { 100 | return eng.BaseFilter.Pre(c) 101 | } 102 | 103 | eng.lastActive = time.Now() 104 | 105 | if len(eng.applied) == 0 { 106 | return eng.BaseFilter.Pre(c) 107 | } 108 | 109 | rc := acquireContext() 110 | rc.delegate = c 111 | for _, rt := range eng.applied { 112 | statusCode, err := rt.Pre(rc) 113 | if nil != err { 114 | releaseContext(rc) 115 | return statusCode, err 116 | } 117 | 118 | if statusCode == filter.BreakFilterChainCode { 119 | releaseContext(rc) 120 | return statusCode, err 121 | } 122 | } 123 | 124 | releaseContext(rc) 125 | return http.StatusOK, nil 126 | } 127 | 128 | // Post filter post method 129 | func (eng *Engine) Post(c filter.Context) (int, error) { 130 | if !eng.enable { 131 | return eng.BaseFilter.Post(c) 132 | } 133 | 134 | eng.lastActive = time.Now() 135 | 136 | if len(eng.applied) == 0 { 137 | return eng.BaseFilter.Post(c) 138 | } 139 | 140 | rc := acquireContext() 141 | rc.delegate = c 142 | 143 | l := len(eng.applied) 144 | for i := l - 1; i >= 0; i-- { 145 | rt := eng.applied[i] 146 | 147 | statusCode, err := rt.Post(rc) 148 | if nil != err { 149 | releaseContext(rc) 150 | return statusCode, err 151 | } 152 | 153 | if statusCode == filter.BreakFilterChainCode { 154 | releaseContext(rc) 155 | return statusCode, err 156 | } 157 | } 158 | 159 | releaseContext(rc) 160 | return http.StatusOK, nil 161 | } 162 | 163 | // PostErr filter post error method 164 | func (eng *Engine) PostErr(c filter.Context, code int, err error) { 165 | if !eng.enable { 166 | eng.BaseFilter.PostErr(c, code, err) 167 | return 168 | } 169 | 170 | eng.lastActive = time.Now() 171 | 172 | if len(eng.applied) == 0 { 173 | eng.BaseFilter.PostErr(c, code, err) 174 | return 175 | } 176 | 177 | rc := acquireContext() 178 | rc.delegate = c 179 | 180 | l := len(eng.applied) 181 | for i := l - 1; i >= 0; i-- { 182 | eng.applied[i].PostErr(rc, code, err) 183 | } 184 | 185 | releaseContext(rc) 186 | } 187 | -------------------------------------------------------------------------------- /pkg/proxy/cfg.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/fagongzi/gateway/pkg/util" 9 | ) 10 | 11 | // Option proxy option 12 | type Option struct { 13 | LimitCountDispatchWorker uint64 14 | LimitCountCopyWorker uint64 15 | LimitCountHeathCheckWorker int 16 | LimitCountConn int 17 | LimitIntervalHeathCheck time.Duration 18 | LimitDurationConnKeepalive time.Duration 19 | LimitDurationConnIdle time.Duration 20 | LimitTimeoutWrite time.Duration 21 | LimitTimeoutRead time.Duration 22 | LimitBufferRead int 23 | LimitBufferWrite int 24 | LimitBytesBody int 25 | LimitBytesCaching uint64 26 | 27 | JWTCfgFile string 28 | CrossCfgFile string 29 | 30 | EnableWebSocket bool 31 | EnableJSPlugin bool 32 | DisableHeaderNameNormalizing bool 33 | } 34 | 35 | // Cfg proxy config 36 | type Cfg struct { 37 | Addr string 38 | AddrHTTPS string 39 | DefaultTLSCert string 40 | DefaultTLSKey string 41 | AddrRPC string 42 | AddrStore string 43 | AddrStoreUserName string 44 | AddrStorePwd string 45 | AddrPPROF string 46 | Namespace string 47 | TTLProxy int64 48 | Filers []*FilterSpec 49 | 50 | Option *Option 51 | Metric *util.MetricCfg 52 | } 53 | 54 | // AddFilter add a filter 55 | func (c *Cfg) AddFilter(filter *FilterSpec) { 56 | c.Filers = append(c.Filers, filter) 57 | } 58 | 59 | // FilterSpec filter spec 60 | type FilterSpec struct { 61 | Name string `json:"name"` 62 | External bool `json:"external,omitempty"` 63 | ExternalPluginFile string `json:"externalPluginFile,omitempty"` 64 | ExternalCfg string `json:"externalCfg,omitempty"` 65 | } 66 | 67 | // ParseFilter returns a filter 68 | func ParseFilter(filter string) (*FilterSpec, error) { 69 | specs := strings.Split(filter, ":") 70 | 71 | switch len(specs) { 72 | case 1: 73 | return &FilterSpec{Name: specs[0]}, nil 74 | case 2: 75 | return &FilterSpec{ 76 | Name: specs[0], 77 | External: true, 78 | ExternalPluginFile: specs[1]}, nil 79 | case 3: 80 | return &FilterSpec{ 81 | Name: specs[0], 82 | External: true, 83 | ExternalPluginFile: specs[1], 84 | ExternalCfg: specs[2]}, nil 85 | default: 86 | return nil, fmt.Errorf("error format: %s", filter) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/proxy/checker.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/fagongzi/gateway/pkg/pb/metapb" 8 | "github.com/fagongzi/gateway/pkg/store" 9 | "github.com/fagongzi/gateway/pkg/util" 10 | "github.com/fagongzi/log" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | func (r *dispatcher) readyToHeathChecker() { 15 | for i := 0; i < r.cnf.Option.LimitCountHeathCheckWorker; i++ { 16 | r.runner.RunCancelableTask(func(ctx context.Context) { 17 | log.Infof("start server check worker") 18 | 19 | for { 20 | select { 21 | case <-ctx.Done(): 22 | return 23 | case id := <-r.checkerC: 24 | r.check(id) 25 | } 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func (r *dispatcher) addToCheck(svr *serverRuntime) { 32 | svr.circuit = metapb.Open 33 | if svr.meta.HeathCheck != nil { 34 | svr.useCheckDuration = time.Duration(svr.meta.HeathCheck.CheckInterval) 35 | } 36 | svr.heathTimeout.Stop() 37 | r.checkerC <- svr.meta.ID 38 | } 39 | 40 | func (r *dispatcher) heathCheckTimeout(arg interface{}) { 41 | id := arg.(uint64) 42 | if _, ok := r.servers[id]; ok { 43 | r.checkerC <- id 44 | } 45 | } 46 | 47 | func (r *dispatcher) check(id uint64) { 48 | svr, ok := r.servers[id] 49 | if !ok { 50 | return 51 | } 52 | 53 | defer func() { 54 | if svr.meta.HeathCheck != nil { 55 | if svr.useCheckDuration > r.cnf.Option.LimitIntervalHeathCheck { 56 | svr.useCheckDuration = r.cnf.Option.LimitIntervalHeathCheck 57 | } 58 | 59 | if svr.useCheckDuration == 0 { 60 | svr.useCheckDuration = time.Duration(svr.meta.HeathCheck.CheckInterval) 61 | } 62 | 63 | svr.heathTimeout, _ = r.tw.Schedule(svr.useCheckDuration, r.heathCheckTimeout, id) 64 | } 65 | }() 66 | 67 | status := metapb.Unknown 68 | prev := r.getServerStatus(svr.meta.ID) 69 | 70 | if svr.meta.HeathCheck == nil { 71 | log.Warnf("server <%d> heath check not setting", svr.meta.ID) 72 | r.watchEventC <- &store.Evt{ 73 | Src: eventSrcStatusChanged, 74 | Type: eventTypeStatusChanged, 75 | Value: statusChanged{ 76 | meta: *svr.meta, 77 | status: metapb.Up, 78 | }, 79 | } 80 | return 81 | } 82 | 83 | if r.doCheck(svr) { 84 | status = metapb.Up 85 | } else { 86 | status = metapb.Down 87 | } 88 | 89 | if prev != status { 90 | r.watchEventC <- &store.Evt{ 91 | Src: eventSrcStatusChanged, 92 | Type: eventTypeStatusChanged, 93 | Value: statusChanged{ 94 | meta: *svr.meta, 95 | status: status, 96 | }, 97 | } 98 | } 99 | } 100 | 101 | func (r *dispatcher) doCheck(svr *serverRuntime) bool { 102 | req := fasthttp.AcquireRequest() 103 | defer fasthttp.ReleaseRequest(req) 104 | 105 | req.SetRequestURI(svr.getCheckURL()) 106 | 107 | opt := util.DefaultHTTPOption() 108 | *opt = *globalHTTPOptions 109 | opt.ReadTimeout = time.Duration(svr.meta.HeathCheck.Timeout) 110 | 111 | resp, err := r.httpClient.Do(req, svr.meta.Addr, opt) 112 | defer fasthttp.ReleaseResponse(resp) 113 | if err != nil { 114 | log.Warnf("server <%d, %s, %d> check failed, errors:\n%+v", 115 | svr.meta.ID, 116 | svr.getCheckURL(), 117 | svr.checkFailCount+1, 118 | err) 119 | svr.fail() 120 | return false 121 | } 122 | 123 | if fasthttp.StatusOK != resp.StatusCode() { 124 | log.Warnf("server <%d, %s, %d, %d> check failed", 125 | svr.meta.ID, 126 | svr.getCheckURL(), 127 | resp.StatusCode(), 128 | svr.checkFailCount+1) 129 | svr.fail() 130 | return false 131 | } 132 | 133 | if svr.meta.HeathCheck.Body != "" && 134 | svr.meta.HeathCheck.Body != string(resp.Body()) { 135 | log.Warnf("server <%s, %s, %d> check failed, body <%s>, expect <%s>", 136 | svr.meta.Addr, 137 | svr.getCheckURL(), 138 | svr.checkFailCount+1, 139 | resp.Body(), 140 | svr.meta.HeathCheck.Body) 141 | svr.fail() 142 | return false 143 | } 144 | 145 | svr.reset() 146 | return true 147 | } 148 | -------------------------------------------------------------------------------- /pkg/proxy/dispatcher_copy_on_write.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/fagongzi/gateway/pkg/route" 6 | ) 7 | 8 | func (r *dispatcher) copyServers(exclude uint64) map[uint64]*serverRuntime { 9 | values := make(map[uint64]*serverRuntime) 10 | for key, value := range r.servers { 11 | if key != exclude { 12 | values[key] = value.clone() 13 | } 14 | } 15 | return values 16 | } 17 | 18 | func (r *dispatcher) copyClusters(exclude uint64) map[uint64]*clusterRuntime { 19 | values := make(map[uint64]*clusterRuntime) 20 | for key, value := range r.clusters { 21 | if key != exclude { 22 | values[key] = value.clone() 23 | } 24 | 25 | } 26 | return values 27 | } 28 | 29 | func (r *dispatcher) copyRoutings(exclude uint64) map[uint64]*routingRuntime { 30 | values := make(map[uint64]*routingRuntime) 31 | for key, value := range r.routings { 32 | if key != exclude { 33 | values[key] = value.clone() 34 | } 35 | } 36 | return values 37 | } 38 | 39 | func (r *dispatcher) copyAPIs(exclude uint64, excludeToRoute uint64) (*route.Route, map[uint64]*apiRuntime) { 40 | route := route.NewRoute() 41 | values := make(map[uint64]*apiRuntime) 42 | for key, value := range r.apis { 43 | if key != exclude { 44 | values[key] = value.clone() 45 | if key != excludeToRoute && value.isUp() { 46 | route.Add(values[key].meta) 47 | } 48 | } 49 | } 50 | 51 | return route, values 52 | } 53 | 54 | func (r *dispatcher) copyBinds(exclude metapb.Bind) map[uint64]*binds { 55 | // remove server from all cluster 56 | removedServer := exclude.ClusterID == 0 57 | 58 | values := make(map[uint64]*binds) 59 | for key, bindsInfo := range r.binds { 60 | if removedServer { 61 | exclude.ClusterID = key 62 | } 63 | 64 | newBindsInfo := &binds{} 65 | for _, info := range bindsInfo.servers { 66 | if info.svrID != exclude.ServerID || exclude.ClusterID != key { 67 | newBindsInfo.servers = append(newBindsInfo.servers, &bindInfo{ 68 | svrID: info.svrID, 69 | status: info.status, 70 | }) 71 | } 72 | } 73 | 74 | for _, info := range bindsInfo.actives { 75 | if info.ID != exclude.ServerID || exclude.ClusterID != key { 76 | newBindsInfo.actives = append(newBindsInfo.actives, info) 77 | } 78 | } 79 | 80 | values[key] = newBindsInfo 81 | } 82 | 83 | return values 84 | } 85 | -------------------------------------------------------------------------------- /pkg/proxy/errors.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // ErrPrefixRequestCancel user cancel request error 9 | ErrPrefixRequestCancel = "request canceled" 10 | // ErrNoServer no server 11 | ErrNoServer = errors.New("has no server") 12 | // ErrRewriteNotMatch rewrite not match request url 13 | ErrRewriteNotMatch = errors.New("rewrite not match request url") 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/proxy/factory.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | "plugin" 6 | "strings" 7 | 8 | "github.com/fagongzi/gateway/pkg/filter" 9 | ) 10 | 11 | var ( 12 | // ErrUnknownFilter unknown filter error 13 | ErrUnknownFilter = errors.New("unknown filter") 14 | ) 15 | 16 | const ( 17 | // FilterPrepare prepare filter 18 | FilterPrepare = "PREPARE" 19 | // FilterHTTPAccess access log filter 20 | FilterHTTPAccess = "HTTP-ACCESS" 21 | // FilterHeader header filter 22 | FilterHeader = "HEADER" // process header fiter 23 | // FilterXForward xforward fiter 24 | FilterXForward = "XFORWARD" 25 | // FilterBlackList blacklist filter 26 | FilterBlackList = "BLACKLIST" 27 | // FilterWhiteList whitelist filter 28 | FilterWhiteList = "WHITELIST" 29 | // FilterAnalysis analysis filter 30 | FilterAnalysis = "ANALYSIS" 31 | // FilterRateLimiting limit filter 32 | FilterRateLimiting = "RATE-LIMITING" 33 | // FilterCircuitBreake circuit breake filter 34 | FilterCircuitBreake = "CIRCUIT-BREAKER" 35 | // FilterValidation validation request filter 36 | FilterValidation = "VALIDATION" 37 | // FilterCaching caching filter 38 | FilterCaching = "CACHING" 39 | // FilterJWT jwt filter 40 | FilterJWT = "JWT" 41 | // FilterCross cross filter 42 | FilterCross = "CROSS" 43 | // FilterJSPlugin js plugin engine 44 | FilterJSPlugin = "JS-ENGINE" 45 | ) 46 | 47 | func (p *Proxy) newFilter(filterSpec *FilterSpec) (filter.Filter, error) { 48 | if filterSpec.External { 49 | return newExternalFilter(filterSpec) 50 | } 51 | 52 | input := strings.ToUpper(filterSpec.Name) 53 | 54 | switch input { 55 | case FilterPrepare: 56 | return newPrepareFilter(), nil 57 | case FilterHTTPAccess: 58 | return newAccessFilter(), nil 59 | case FilterHeader: 60 | return newHeadersFilter(), nil 61 | case FilterXForward: 62 | return newXForwardForFilter(), nil 63 | case FilterAnalysis: 64 | return newAnalysisFilter(), nil 65 | case FilterBlackList: 66 | return newBlackListFilter(), nil 67 | case FilterWhiteList: 68 | return newWhiteListFilter(), nil 69 | case FilterRateLimiting: 70 | return newRateLimitingFilter(), nil 71 | case FilterCircuitBreake: 72 | return newCircuitBreakeFilter(), nil 73 | case FilterValidation: 74 | return newValidationFilter(), nil 75 | case FilterCaching: 76 | return newCachingFilter(p.cfg.Option.LimitBytesCaching, p.dispatcher.tw), nil 77 | case FilterJWT: 78 | return newJWTFilter(p.cfg.Option.JWTCfgFile) 79 | case FilterCross: 80 | return newCrossDomainFilter(p.cfg.Option.CrossCfgFile) 81 | case FilterJSPlugin: 82 | return p.jsEngine, nil 83 | default: 84 | return nil, ErrUnknownFilter 85 | } 86 | } 87 | 88 | func newExternalFilter(filterSpec *FilterSpec) (filter.Filter, error) { 89 | p, err := plugin.Open(filterSpec.ExternalPluginFile) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | s, err := p.Lookup("NewExternalFilter") 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | sf := s.(func() (filter.Filter, error)) 100 | return sf() 101 | } 102 | -------------------------------------------------------------------------------- /pkg/proxy/filter.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/fagongzi/gateway/pkg/filter" 8 | "github.com/fagongzi/gateway/pkg/pb/metapb" 9 | "github.com/fagongzi/gateway/pkg/util" 10 | "github.com/fagongzi/log" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | func (f *Proxy) doPreFilters(requestTag string, c filter.Context, filters ...filter.Filter) (filterName string, statusCode int, err error) { 15 | for _, f := range filters { 16 | filterName = f.Name() 17 | 18 | statusCode, err = f.Pre(c) 19 | if nil != err { 20 | return filterName, statusCode, err 21 | } 22 | 23 | if statusCode == filter.BreakFilterChainCode { 24 | log.Debugf("%s: break pre filter chain by filter %s", 25 | requestTag, 26 | filterName) 27 | return filterName, statusCode, err 28 | } 29 | } 30 | 31 | return "", http.StatusOK, nil 32 | } 33 | 34 | func (f *Proxy) doPostFilters(requestTag string, c filter.Context, filters ...filter.Filter) (filterName string, statusCode int, err error) { 35 | l := len(filters) 36 | for i := l - 1; i >= 0; i-- { 37 | f := filters[i] 38 | statusCode, err = f.Post(c) 39 | if nil != err { 40 | return filterName, statusCode, err 41 | } 42 | 43 | if statusCode == filter.BreakFilterChainCode { 44 | log.Debugf("%s: break post filter chain by filter %s", 45 | requestTag, 46 | filterName) 47 | return filterName, statusCode, err 48 | } 49 | } 50 | 51 | return "", http.StatusOK, nil 52 | } 53 | 54 | func (f *Proxy) doPostErrFilters(c filter.Context, code int, err error, filters ...filter.Filter) { 55 | l := len(filters) 56 | for i := l - 1; i >= 0; i-- { 57 | f := filters[i] 58 | f.PostErr(c, code, err) 59 | } 60 | } 61 | 62 | type proxyContext struct { 63 | startAt time.Time 64 | endAt time.Time 65 | result *dispatchNode 66 | forwardReq *fasthttp.Request 67 | originCtx *fasthttp.RequestCtx 68 | rt *dispatcher 69 | 70 | attrs map[string]interface{} 71 | } 72 | 73 | func (c *proxyContext) init(rt *dispatcher, originCtx *fasthttp.RequestCtx, forwardReq *fasthttp.Request, result *dispatchNode) { 74 | c.result = result 75 | c.originCtx = originCtx 76 | c.forwardReq = forwardReq 77 | c.rt = rt 78 | c.startAt = time.Now() 79 | c.attrs = make(map[string]interface{}) 80 | } 81 | 82 | func (c *proxyContext) reset() { 83 | if c.forwardReq != nil { 84 | fasthttp.ReleaseRequest(c.forwardReq) 85 | } 86 | *c = emptyContext 87 | } 88 | 89 | func (c *proxyContext) SetAttr(key string, value interface{}) { 90 | c.attrs[key] = value 91 | } 92 | 93 | func (c *proxyContext) GetAttr(key string) interface{} { 94 | return c.attrs[key] 95 | } 96 | 97 | func (c *proxyContext) StartAt() time.Time { 98 | return c.startAt 99 | } 100 | 101 | func (c *proxyContext) EndAt() time.Time { 102 | return c.endAt 103 | } 104 | 105 | func (c *proxyContext) DispatchNode() *metapb.DispatchNode { 106 | return c.result.node.meta 107 | } 108 | 109 | func (c *proxyContext) API() *metapb.API { 110 | return c.result.api.meta 111 | } 112 | 113 | func (c *proxyContext) Server() *metapb.Server { 114 | return c.result.dest.meta 115 | } 116 | 117 | func (c *proxyContext) ForwardRequest() *fasthttp.Request { 118 | return c.forwardReq 119 | } 120 | 121 | func (c *proxyContext) Response() *fasthttp.Response { 122 | return c.result.res 123 | } 124 | 125 | func (c *proxyContext) OriginRequest() *fasthttp.RequestCtx { 126 | return c.originCtx 127 | } 128 | 129 | func (c *proxyContext) Analysis() *util.Analysis { 130 | return c.rt.analysiser 131 | } 132 | 133 | func (c *proxyContext) setEndAt(endAt time.Time) { 134 | c.endAt = endAt 135 | } 136 | 137 | func (c *proxyContext) validateRequest() bool { 138 | return c.result.node.validate(c.ForwardRequest()) 139 | } 140 | 141 | func (c *proxyContext) allowWithBlacklist(ip string) bool { 142 | return c.result.api.allowWithBlacklist(ip) 143 | } 144 | 145 | func (c *proxyContext) allowWithWhitelist(ip string) bool { 146 | return c.result.api.allowWithWhitelist(ip) 147 | } 148 | 149 | func (c *proxyContext) circuitResourceID() uint64 { 150 | if c.result.api.cb != nil { 151 | return c.result.api.id 152 | } 153 | 154 | return c.result.dest.id 155 | } 156 | 157 | func (c *proxyContext) rateLimiter() *rateLimiter { 158 | if c.result.api.limiter != nil { 159 | return c.result.api.limiter 160 | } 161 | 162 | return c.result.dest.limiter 163 | } 164 | 165 | func (c *proxyContext) circuitBreaker() (*metapb.CircuitBreaker, *util.RateBarrier) { 166 | if c.result.api.cb != nil { 167 | return c.result.api.cb, c.result.api.barrier 168 | } 169 | 170 | return c.result.dest.cb, c.result.dest.barrier 171 | } 172 | 173 | func (c *proxyContext) circuitStatus() metapb.CircuitStatus { 174 | if c.result.api.cb != nil { 175 | return c.result.api.getCircuitStatus() 176 | } 177 | 178 | return c.result.dest.getCircuitStatus() 179 | } 180 | 181 | func (c *proxyContext) changeCircuitStatusToClose() { 182 | if c.result.api.cb != nil { 183 | c.result.api.circuitToClose() 184 | } 185 | 186 | c.result.dest.circuitToClose() 187 | } 188 | 189 | func (c *proxyContext) changeCircuitStatusToOpen() { 190 | if c.result.api.cb != nil { 191 | c.result.api.circuitToOpen() 192 | } 193 | 194 | c.result.dest.circuitToOpen() 195 | } 196 | -------------------------------------------------------------------------------- /pkg/proxy/filter_access.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/filter" 5 | "github.com/fagongzi/log" 6 | ) 7 | 8 | // AccessFilter record the http access log 9 | // log format: $remoteip "$method $path" $code "$agent" $svr $cost 10 | type AccessFilter struct { 11 | filter.BaseFilter 12 | } 13 | 14 | func newAccessFilter() filter.Filter { 15 | return &AccessFilter{} 16 | } 17 | 18 | // Init init filter 19 | func (f *AccessFilter) Init(cfg string) error { 20 | return nil 21 | } 22 | 23 | // Name return name of this filter 24 | func (f *AccessFilter) Name() string { 25 | return FilterHTTPAccess 26 | } 27 | 28 | // Post execute after proxy 29 | func (f *AccessFilter) Post(c filter.Context) (statusCode int, err error) { 30 | cost := c.EndAt().Sub(c.StartAt()) 31 | 32 | if log.InfoEnabled() { 33 | log.Infof("filter: %s %s \"%s\" %d \"%s\" %s %s", 34 | filter.StringValue(filter.AttrClientRealIP, c), 35 | c.OriginRequest().Method(), 36 | c.ForwardRequest().RequestURI(), 37 | c.Response().StatusCode(), 38 | c.OriginRequest().UserAgent(), 39 | c.Server().Addr, 40 | cost) 41 | } 42 | 43 | return f.BaseFilter.Post(c) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/proxy/filter_analysis.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/filter" 5 | ) 6 | 7 | // AnalysisFilter analysis filter 8 | type AnalysisFilter struct { 9 | filter.BaseFilter 10 | } 11 | 12 | func newAnalysisFilter() filter.Filter { 13 | return &AnalysisFilter{} 14 | } 15 | 16 | // Init init filter 17 | func (f *AnalysisFilter) Init(cfg string) error { 18 | return nil 19 | } 20 | 21 | // Name return name of this filter 22 | func (f *AnalysisFilter) Name() string { 23 | return FilterAnalysis 24 | } 25 | 26 | // Pre execute before proxy 27 | func (f *AnalysisFilter) Pre(c filter.Context) (statusCode int, err error) { 28 | // TODO: avoid lock overhead in every request 29 | c.Analysis().Request(c.(*proxyContext).circuitResourceID()) 30 | return f.BaseFilter.Pre(c) 31 | } 32 | 33 | // Post execute after proxy 34 | func (f *AnalysisFilter) Post(c filter.Context) (statusCode int, err error) { 35 | c.Analysis().Response(c.(*proxyContext).circuitResourceID(), c.EndAt().Sub(c.StartAt()).Nanoseconds()) 36 | return f.BaseFilter.Post(c) 37 | } 38 | 39 | // PostErr execute proxy has errors 40 | func (f *AnalysisFilter) PostErr(c filter.Context, code int, err error) { 41 | c.Analysis().Failure(c.(*proxyContext).circuitResourceID()) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/proxy/filter_blacklist.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/fagongzi/gateway/pkg/filter" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | var ( 11 | // ErrBlacklist target ip in black list 12 | ErrBlacklist = errors.New("Err, target ip in black list") 13 | ) 14 | 15 | // BlackListFilter blacklist filter 16 | type BlackListFilter struct { 17 | filter.BaseFilter 18 | } 19 | 20 | func newBlackListFilter() filter.Filter { 21 | return &BlackListFilter{} 22 | } 23 | 24 | // Init init filter 25 | func (f *BlackListFilter) Init(cfg string) error { 26 | return nil 27 | } 28 | 29 | // Name return name of this filter 30 | func (f *BlackListFilter) Name() string { 31 | return FilterBlackList 32 | } 33 | 34 | // Pre execute before proxy 35 | func (f *BlackListFilter) Pre(c filter.Context) (statusCode int, err error) { 36 | if !c.(*proxyContext).allowWithBlacklist(filter.StringValue(filter.AttrClientRealIP, c)) { 37 | return fasthttp.StatusForbidden, ErrBlacklist 38 | } 39 | 40 | return f.BaseFilter.Pre(c) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/proxy/filter_caching.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | 8 | "github.com/fagongzi/gateway/pkg/filter" 9 | "github.com/fagongzi/gateway/pkg/pb/metapb" 10 | "github.com/fagongzi/gateway/pkg/util" 11 | "github.com/fagongzi/goetty" 12 | "github.com/fagongzi/util/hack" 13 | "github.com/valyala/fasthttp" 14 | ) 15 | 16 | var ( 17 | cachePool sync.Pool 18 | ) 19 | 20 | // CachingFilter cache api result 21 | type CachingFilter struct { 22 | filter.BaseFilter 23 | 24 | tw *goetty.TimeoutWheel 25 | cache *util.Cache 26 | } 27 | 28 | func newCachingFilter(maxBytes uint64, tw *goetty.TimeoutWheel) filter.Filter { 29 | f := &CachingFilter{ 30 | tw: tw, 31 | } 32 | 33 | f.cache = util.NewLRUCache(maxBytes, f.onEvicted) 34 | return f 35 | } 36 | 37 | // Name return name of this filter 38 | func (f *CachingFilter) Name() string { 39 | return FilterCaching 40 | } 41 | 42 | // Pre execute before proxy 43 | func (f *CachingFilter) Pre(c filter.Context) (statusCode int, err error) { 44 | if c.DispatchNode().Cache == nil { 45 | return f.BaseFilter.Post(c) 46 | } 47 | 48 | matches, id := getCachingID(c) 49 | if !matches { 50 | return f.BaseFilter.Post(c) 51 | } 52 | 53 | if value, ok := f.cache.Get(id); ok { 54 | c.SetAttr(filter.AttrUsingCachingValue, value) 55 | } 56 | 57 | return f.BaseFilter.Post(c) 58 | } 59 | 60 | // Post execute after proxy 61 | func (f *CachingFilter) Post(c filter.Context) (statusCode int, err error) { 62 | if c.DispatchNode().Cache == nil { 63 | return f.BaseFilter.Post(c) 64 | } 65 | 66 | matches, id := getCachingID(c) 67 | if !matches { 68 | return f.BaseFilter.Post(c) 69 | } 70 | 71 | f.cache.Add(id, genCachedValue(c)) 72 | if c.DispatchNode().Cache.Deadline > 0 { 73 | f.tw.Schedule(time.Duration(c.DispatchNode().Cache.Deadline), 74 | f.removeCache, id) 75 | } 76 | 77 | return f.BaseFilter.Post(c) 78 | } 79 | 80 | func (f *CachingFilter) removeCache(id interface{}) { 81 | f.cache.Remove(id) 82 | } 83 | 84 | func (f *CachingFilter) onEvicted(key util.Key, value *goetty.ByteBuf) { 85 | f.tw.Schedule(time.Second*10, f.doReleaseCacheBuf, value) 86 | } 87 | 88 | func (f *CachingFilter) doReleaseCacheBuf(arg interface{}) { 89 | arg.(*goetty.ByteBuf).Release() 90 | } 91 | 92 | func getCachingID(c filter.Context) (bool, string) { 93 | req := c.ForwardRequest() 94 | if len(c.DispatchNode().Cache.Conditions) == 0 { 95 | return true, getID(req, c.DispatchNode().Cache.Keys) 96 | } 97 | 98 | matches := true 99 | for _, cond := range c.DispatchNode().Cache.Conditions { 100 | matches = conditionsMatches(&cond, req) 101 | if !matches { 102 | break 103 | } 104 | } 105 | 106 | if !matches { 107 | return false, "" 108 | } 109 | 110 | return matches, getID(req, c.DispatchNode().Cache.Keys) 111 | } 112 | 113 | func getID(req *fasthttp.Request, keys []metapb.Parameter) string { 114 | size := len(keys) 115 | if size == 0 { 116 | return hack.SliceToString(req.RequestURI()) 117 | } 118 | 119 | ids := make([]string, size+1, size+1) 120 | ids[0] = hack.SliceToString(req.RequestURI()) 121 | for idx, param := range keys { 122 | ids[idx+1] = paramValue(¶m, req) 123 | } 124 | 125 | return strings.Join(ids, "-") 126 | } 127 | 128 | func genCachedValue(c filter.Context) *goetty.ByteBuf { 129 | return filter.NewCachedValue(c.Response()) 130 | } 131 | -------------------------------------------------------------------------------- /pkg/proxy/filter_circuit_breaker.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/fagongzi/gateway/pkg/filter" 10 | "github.com/fagongzi/gateway/pkg/pb/metapb" 11 | ) 12 | 13 | var ( 14 | // ErrCircuitClose resource is in circuit close 15 | ErrCircuitClose = errors.New("resource is in circuit close") 16 | // ErrCircuitHalfLimited resource is in circuit half, traffic limit 17 | ErrCircuitHalfLimited = errors.New("resource is in circuit half, traffic limit") 18 | ) 19 | 20 | // CircuitBreakeFilter CircuitBreakeFilter 21 | type CircuitBreakeFilter struct { 22 | filter.BaseFilter 23 | } 24 | 25 | func newCircuitBreakeFilter() filter.Filter { 26 | return &CircuitBreakeFilter{} 27 | } 28 | 29 | // Init init filter 30 | func (f *CircuitBreakeFilter) Init(cfg string) error { 31 | return nil 32 | } 33 | 34 | // Name return name of this filter 35 | func (f *CircuitBreakeFilter) Name() string { 36 | return FilterCircuitBreake 37 | } 38 | 39 | // Pre execute before proxy 40 | func (f *CircuitBreakeFilter) Pre(c filter.Context) (statusCode int, err error) { 41 | pc := c.(*proxyContext) 42 | cb, barrier := pc.circuitBreaker() 43 | if cb == nil { 44 | return f.BaseFilter.Pre(c) 45 | } 46 | 47 | protectedResourceStatus := pc.circuitStatus() 48 | protectedResource := pc.circuitResourceID() 49 | 50 | switch protectedResourceStatus { 51 | case metapb.Open: 52 | if c.Analysis().GetRecentlyRequestFailureRate(protectedResource, time.Duration(cb.RateCheckPeriod)) >= int(cb.FailureRateToClose) { 53 | pc.changeCircuitStatusToClose() 54 | c.Analysis().Reject(protectedResource) 55 | return http.StatusServiceUnavailable, ErrCircuitClose 56 | } 57 | 58 | return http.StatusOK, nil 59 | case metapb.Half: 60 | if barrier.Allow() { 61 | return f.BaseFilter.Pre(c) 62 | } 63 | 64 | c.Analysis().Reject(protectedResource) 65 | return http.StatusServiceUnavailable, ErrCircuitHalfLimited 66 | default: 67 | c.Analysis().Reject(protectedResource) 68 | return http.StatusServiceUnavailable, ErrCircuitClose 69 | } 70 | } 71 | 72 | // Post execute after proxy 73 | func (f *CircuitBreakeFilter) Post(c filter.Context) (statusCode int, err error) { 74 | pc := c.(*proxyContext) 75 | cb, _ := pc.circuitBreaker() 76 | if cb == nil { 77 | return f.BaseFilter.Post(c) 78 | } 79 | 80 | protectedResourceStatus := pc.circuitStatus() 81 | protectedResource := pc.circuitResourceID() 82 | 83 | if protectedResourceStatus == metapb.Half && 84 | c.Analysis().GetRecentlyRequestSuccessedRate(protectedResource, time.Duration(cb.RateCheckPeriod)) >= int(cb.SucceedRateToOpen) { 85 | pc.changeCircuitStatusToOpen() 86 | } 87 | 88 | return f.BaseFilter.Post(c) 89 | } 90 | 91 | // PostErr execute proxy has errors 92 | func (f *CircuitBreakeFilter) PostErr(c filter.Context, code int, err error) { 93 | // ignore user cancel 94 | if nil != err && strings.HasPrefix(err.Error(), ErrPrefixRequestCancel) { 95 | f.BaseFilter.PostErr(c, code, err) 96 | return 97 | } 98 | 99 | pc := c.(*proxyContext) 100 | cb, _ := pc.circuitBreaker() 101 | if cb == nil { 102 | f.BaseFilter.PostErr(c, code, err) 103 | return 104 | } 105 | 106 | protectedResourceStatus := pc.circuitStatus() 107 | protectedResource := pc.circuitResourceID() 108 | 109 | if protectedResourceStatus == metapb.Half && 110 | c.Analysis().GetRecentlyRequestFailureRate(protectedResource, time.Duration(cb.RateCheckPeriod)) >= int(cb.FailureRateToClose) { 111 | pc.changeCircuitStatusToClose() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/proxy/filter_cross_domain.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | 8 | "github.com/fagongzi/gateway/pkg/filter" 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | var ( 13 | options = []byte("OPTIONS") 14 | ) 15 | 16 | // CrossCfg cross cfg 17 | type CrossCfg struct { 18 | Headers []CrossHeader `json:"headers"` 19 | } 20 | 21 | // CrossHeader cross header 22 | type CrossHeader struct { 23 | Name string `json:"name"` 24 | Value string `json:"value"` 25 | } 26 | 27 | // CrossDomainFilter cross domain 28 | type CrossDomainFilter struct { 29 | filter.BaseFilter 30 | cfg CrossCfg 31 | } 32 | 33 | func newCrossDomainFilter(file string) (filter.Filter, error) { 34 | f := &CrossDomainFilter{} 35 | 36 | err := f.parseCfg(file) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return f, nil 42 | } 43 | 44 | func (f *CrossDomainFilter) parseCfg(file string) error { 45 | data, err := ioutil.ReadFile(file) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | err = json.Unmarshal(data, &f.cfg) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // Name return name of this filter 59 | func (f *CrossDomainFilter) Name() string { 60 | return FilterCross 61 | } 62 | 63 | // Pre execute before proxy 64 | func (f *CrossDomainFilter) Pre(c filter.Context) (statusCode int, err error) { 65 | if bytes.Compare(c.OriginRequest().Method(), options) != 0 { 66 | return f.BaseFilter.Pre(c) 67 | } 68 | 69 | resp := fasthttp.AcquireResponse() 70 | for _, h := range f.cfg.Headers { 71 | resp.Header.Add(h.Name, h.Value) 72 | } 73 | 74 | c.SetAttr(filter.AttrUsingResponse, resp) 75 | return filter.BreakFilterChainCode, nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/proxy/filter_headers.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/filter" 5 | ) 6 | 7 | // Hop-by-hop headers. These are removed when sent to the backend. 8 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html 9 | var hopHeaders = []string{ 10 | //"Connection", 11 | "Keep-Alive", 12 | "Proxy-Authenticate", 13 | "Proxy-Authorization", 14 | "Te", 15 | "Trailers", 16 | "Transfer-Encoding", 17 | } 18 | 19 | // HeadersFilter HeadersFilter 20 | type HeadersFilter struct { 21 | filter.BaseFilter 22 | } 23 | 24 | func newHeadersFilter() filter.Filter { 25 | return &HeadersFilter{} 26 | } 27 | 28 | // Init init filter 29 | func (f *HeadersFilter) Init(cfg string) error { 30 | return nil 31 | } 32 | 33 | // Name return name of this filter 34 | func (f *HeadersFilter) Name() string { 35 | return FilterHeader 36 | } 37 | 38 | // Pre execute before proxy 39 | func (f *HeadersFilter) Pre(c filter.Context) (statusCode int, err error) { 40 | for _, h := range hopHeaders { 41 | c.ForwardRequest().Header.Del(h) 42 | } 43 | 44 | c.ForwardRequest().Header.SetHost(c.Server().Addr) 45 | return f.BaseFilter.Pre(c) 46 | } 47 | 48 | // Post execute after proxy 49 | func (f *HeadersFilter) Post(c filter.Context) (statusCode int, err error) { 50 | for _, h := range hopHeaders { 51 | c.Response().Header.Del(h) 52 | } 53 | 54 | // 需要合并处理的,不做header的复制,由proxy做合并 55 | if len(c.API().Nodes) == 1 { 56 | c.OriginRequest().Response.Header.Reset() 57 | c.Response().Header.CopyTo(&c.OriginRequest().Response.Header) 58 | } 59 | 60 | return f.BaseFilter.Post(c) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/proxy/filter_prepare.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/filter" 5 | "github.com/fagongzi/gateway/pkg/util" 6 | ) 7 | 8 | // PrepareFilter Must be in the first of the filter chain, 9 | // used to get some public information into the context, 10 | // to avoid subsequent filters to do duplicate things. 11 | type PrepareFilter struct { 12 | filter.BaseFilter 13 | } 14 | 15 | func newPrepareFilter() filter.Filter { 16 | return &PrepareFilter{} 17 | } 18 | 19 | // Init init filter 20 | func (f *PrepareFilter) Init(cfg string) error { 21 | return nil 22 | } 23 | 24 | // Name return name of this filter 25 | func (f *PrepareFilter) Name() string { 26 | return FilterPrepare 27 | } 28 | 29 | // Pre execute before proxy 30 | func (f *PrepareFilter) Pre(c filter.Context) (statusCode int, err error) { 31 | c.SetAttr(filter.AttrClientRealIP, util.ClientIP(c.OriginRequest())) 32 | return f.BaseFilter.Pre(c) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/proxy/filter_rate_limiting.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/fagongzi/gateway/pkg/filter" 8 | ) 9 | 10 | var ( 11 | errOverLimit = errors.New("too many requests") 12 | ) 13 | 14 | // RateLimitingFilter RateLimitingFilter 15 | type RateLimitingFilter struct { 16 | filter.BaseFilter 17 | } 18 | 19 | func newRateLimitingFilter() filter.Filter { 20 | return &RateLimitingFilter{} 21 | } 22 | 23 | // Init init filter 24 | func (f *RateLimitingFilter) Init(cfg string) error { 25 | return nil 26 | } 27 | 28 | // Name return name of this filter 29 | func (f *RateLimitingFilter) Name() string { 30 | return FilterRateLimiting 31 | } 32 | 33 | // Pre execute before proxy 34 | func (f *RateLimitingFilter) Pre(c filter.Context) (statusCode int, err error) { 35 | if !c.(*proxyContext).rateLimiter().do(1) { 36 | return http.StatusTooManyRequests, errOverLimit 37 | } 38 | 39 | return f.BaseFilter.Pre(c) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/proxy/filter_validation.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/fagongzi/gateway/pkg/filter" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | var ( 11 | // ErrValidationFailure validation failure 12 | ErrValidationFailure = errors.New("request validation failure") 13 | ) 14 | 15 | // ValidationFilter validation request 16 | type ValidationFilter struct { 17 | filter.BaseFilter 18 | } 19 | 20 | func newValidationFilter() filter.Filter { 21 | return &ValidationFilter{} 22 | } 23 | 24 | // Init init filter 25 | func (f *ValidationFilter) Init(cfg string) error { 26 | return nil 27 | } 28 | 29 | // Name return name of this filter 30 | func (f *ValidationFilter) Name() string { 31 | return FilterValidation 32 | } 33 | 34 | // Pre pre filter, before proxy reuqest 35 | func (f *ValidationFilter) Pre(c filter.Context) (statusCode int, err error) { 36 | if c.(*proxyContext).validateRequest() { 37 | return f.BaseFilter.Pre(c) 38 | } 39 | 40 | return fasthttp.StatusBadRequest, ErrValidationFailure 41 | } 42 | -------------------------------------------------------------------------------- /pkg/proxy/filter_whitelist.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/fagongzi/gateway/pkg/filter" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | var ( 11 | // ErrWhitelist target ip not in in white list 12 | ErrWhitelist = errors.New("Err, target ip not in in white list") 13 | ) 14 | 15 | // WhiteListFilter whitelist filter 16 | type WhiteListFilter struct { 17 | filter.BaseFilter 18 | } 19 | 20 | func newWhiteListFilter() filter.Filter { 21 | return &WhiteListFilter{} 22 | } 23 | 24 | // Init init filter 25 | func (f *WhiteListFilter) Init(cfg string) error { 26 | return nil 27 | } 28 | 29 | // Name return name of this filter 30 | func (f *WhiteListFilter) Name() string { 31 | return FilterWhiteList 32 | } 33 | 34 | // Pre execute before proxy 35 | func (f *WhiteListFilter) Pre(c filter.Context) (statusCode int, err error) { 36 | if !c.(*proxyContext).allowWithWhitelist(filter.StringValue(filter.AttrClientRealIP, c)) { 37 | return fasthttp.StatusForbidden, ErrWhitelist 38 | } 39 | 40 | return f.BaseFilter.Pre(c) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/proxy/filter_xforwardfor.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/fagongzi/gateway/pkg/filter" 7 | "github.com/fagongzi/util/hack" 8 | ) 9 | 10 | var ( 11 | headerName = []byte("X-Forwarded-For") 12 | ) 13 | 14 | // XForwardForFilter XForwardForFilter 15 | type XForwardForFilter struct { 16 | filter.BaseFilter 17 | } 18 | 19 | func newXForwardForFilter() filter.Filter { 20 | return &XForwardForFilter{} 21 | } 22 | 23 | // Init init filter 24 | func (f *XForwardForFilter) Init(cfg string) error { 25 | return nil 26 | } 27 | 28 | // Name return name of this filter 29 | func (f *XForwardForFilter) Name() string { 30 | return FilterXForward 31 | } 32 | 33 | // Pre execute before proxy 34 | func (f *XForwardForFilter) Pre(c filter.Context) (statusCode int, err error) { 35 | prevForward := c.OriginRequest().Request.Header.PeekBytes(headerName) 36 | if len(prevForward) == 0 { 37 | c.ForwardRequest().Header.SetBytesKV(headerName, hack.StringToSlice(c.OriginRequest().RemoteIP().String())) 38 | } else { 39 | var buf bytes.Buffer 40 | buf.Write(prevForward) 41 | buf.WriteByte(',') 42 | buf.WriteByte(' ') 43 | buf.WriteString(c.OriginRequest().RemoteIP().String()) 44 | c.ForwardRequest().Header.SetBytesKV(headerName, buf.Bytes()) 45 | } 46 | 47 | return f.BaseFilter.Pre(c) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/proxy/io.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "sync" 7 | "time" 8 | 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | type writeFlusher interface { 13 | io.Writer 14 | http.Flusher 15 | } 16 | 17 | type maxLatencyWriter struct { 18 | dst writeFlusher 19 | latency time.Duration 20 | 21 | lk sync.Mutex // protects Write + Flush 22 | done chan bool 23 | } 24 | 25 | func (m *maxLatencyWriter) Write(p []byte) (int, error) { 26 | m.lk.Lock() 27 | defer m.lk.Unlock() 28 | return m.dst.Write(p) 29 | } 30 | 31 | func (m *maxLatencyWriter) flushLoop() { 32 | t := time.NewTicker(m.latency) 33 | defer t.Stop() 34 | for { 35 | select { 36 | case <-m.done: 37 | if onExitFlushLoop != nil { 38 | onExitFlushLoop() 39 | } 40 | return 41 | case <-t.C: 42 | m.lk.Lock() 43 | m.dst.Flush() 44 | m.lk.Unlock() 45 | } 46 | } 47 | } 48 | 49 | func (m *maxLatencyWriter) stop() { m.done <- true } 50 | 51 | // onExitFlushLoop is a callback set by tests to detect the state of the 52 | // flushLoop() goroutine. 53 | var onExitFlushLoop func() 54 | 55 | func copyHeader(dst, src http.Header) { 56 | for k, vv := range src { 57 | for _, v := range vv { 58 | dst.Add(k, v) 59 | } 60 | } 61 | } 62 | 63 | type requestCanceler interface { 64 | CancelRequest(*http.Request) 65 | } 66 | 67 | type runOnFirstRead struct { 68 | io.Reader 69 | 70 | fn func() // Run before first Read, then set to nil 71 | } 72 | 73 | func (c *runOnFirstRead) Read(bs []byte) (int, error) { 74 | if c.fn != nil { 75 | c.fn() 76 | c.fn = nil 77 | } 78 | return c.Reader.Read(bs) 79 | } 80 | 81 | func copyRequest(req *fasthttp.Request) *fasthttp.Request { 82 | newreq := fasthttp.AcquireRequest() 83 | newreq.Reset() 84 | req.CopyTo(newreq) 85 | return newreq 86 | } 87 | -------------------------------------------------------------------------------- /pkg/proxy/metric.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | const ( 11 | typeRequestAll = "all" 12 | typeRequestFail = "fail" 13 | typeRequestSucceed = "succeed" 14 | typeRequestLimit = "limit" 15 | typeRequestReject = "reject" 16 | ) 17 | 18 | var ( 19 | apiRequestCounterVec = prometheus.NewCounterVec( 20 | prometheus.CounterOpts{ 21 | Namespace: "gateway", 22 | Subsystem: "proxy", 23 | Name: "api_request_total", 24 | Help: "Total number of request made.", 25 | }, []string{"name", "type"}) 26 | 27 | apiResponseHistogramVec = prometheus.NewHistogramVec( 28 | prometheus.HistogramOpts{ 29 | Namespace: "gateway", 30 | Subsystem: "proxy", 31 | Name: "api_response_duration_seconds", 32 | Help: "Bucketed histogram of api response time duration", 33 | Buckets: prometheus.ExponentialBuckets(0.0005, 2.0, 20), 34 | }, []string{"name"}) 35 | ) 36 | 37 | func init() { 38 | prometheus.Register(apiRequestCounterVec) 39 | prometheus.Register(apiResponseHistogramVec) 40 | } 41 | 42 | func (p *Proxy) postRequest(api *apiRuntime, dispatches []*dispatchNode, startAt time.Time) { 43 | doMetrics := true 44 | for _, dn := range dispatches { 45 | if doMetrics && 46 | (dn.err == ErrCircuitClose || dn.err == ErrBlacklist || dn.err == ErrWhitelist) { 47 | incrRequestReject(api.meta.Name) 48 | doMetrics = false 49 | } else if doMetrics && dn.err == ErrCircuitHalfLimited { 50 | incrRequestLimit(api.meta.Name) 51 | doMetrics = false 52 | } else if doMetrics && dn.err != nil { 53 | incrRequestFailed(api.meta.Name) 54 | doMetrics = false 55 | } else if doMetrics && dn.code >= fasthttp.StatusBadRequest { 56 | incrRequestFailed(api.meta.Name) 57 | doMetrics = false 58 | } 59 | 60 | releaseDispathNode(dn) 61 | } 62 | 63 | if doMetrics { 64 | incrRequestSucceed(api.meta.Name) 65 | observeAPIResponse(api.meta.Name, startAt) 66 | } 67 | } 68 | 69 | func incrRequest(name string) { 70 | apiRequestCounterVec.WithLabelValues(name, typeRequestAll).Inc() 71 | } 72 | 73 | func incrRequestFailed(name string) { 74 | apiRequestCounterVec.WithLabelValues(name, typeRequestFail).Inc() 75 | } 76 | 77 | func incrRequestSucceed(name string) { 78 | apiRequestCounterVec.WithLabelValues(name, typeRequestSucceed).Inc() 79 | } 80 | 81 | func incrRequestLimit(name string) { 82 | apiRequestCounterVec.WithLabelValues(name, typeRequestLimit).Inc() 83 | } 84 | 85 | func incrRequestReject(name string) { 86 | apiRequestCounterVec.WithLabelValues(name, typeRequestReject).Inc() 87 | } 88 | 89 | func observeAPIResponse(name string, startAt time.Time) { 90 | now := time.Now() 91 | apiResponseHistogramVec.WithLabelValues(name).Observe(now.Sub(startAt).Seconds()) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/proxy/multi.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/buger/jsonparser" 7 | "github.com/fagongzi/log" 8 | "github.com/fagongzi/util/hack" 9 | ) 10 | 11 | type multiContext struct { 12 | sync.RWMutex 13 | data []byte 14 | } 15 | 16 | func (c *multiContext) reset() { 17 | c.init() 18 | } 19 | 20 | func (c *multiContext) init() { 21 | c.data = emptyObject 22 | } 23 | 24 | func (c *multiContext) completePart(attr string, data []byte) { 25 | c.Lock() 26 | if len(data) > 0 && attr != "" { 27 | c.data, _ = jsonparser.Set(c.data, data, attr) 28 | } 29 | c.Unlock() 30 | } 31 | 32 | func (c *multiContext) getAttr(paths ...string) string { 33 | c.RLock() 34 | value, _, _, err := jsonparser.Get(c.data, paths...) 35 | c.RUnlock() 36 | if err != nil { 37 | log.Errorf("extract %+v failed, errors:\n%+v", paths, err) 38 | return "" 39 | } 40 | 41 | return hack.SliceToString(value) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/proxy/pool.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/fagongzi/gateway/pkg/expr" 7 | "github.com/fagongzi/goetty" 8 | ) 9 | 10 | var ( 11 | renderPool sync.Pool 12 | contextPool sync.Pool 13 | dispatchNodePool sync.Pool 14 | multiContextPool sync.Pool 15 | wgPool sync.Pool 16 | exprCtxPool sync.Pool 17 | bytesPool = goetty.NewSyncPool(2, 1024*1024*5, 2) 18 | 19 | emptyRender = render{} 20 | emptyContext = proxyContext{} 21 | emptyDispathNode = dispatchNode{} 22 | ) 23 | 24 | func acquireWG() *sync.WaitGroup { 25 | v := wgPool.Get() 26 | if v == nil { 27 | return &sync.WaitGroup{} 28 | } 29 | 30 | return v.(*sync.WaitGroup) 31 | } 32 | 33 | func releaseWG(value *sync.WaitGroup) { 34 | if value != nil { 35 | wgPool.Put(value) 36 | } 37 | } 38 | 39 | func acquireMultiContext() *multiContext { 40 | v := multiContextPool.Get() 41 | if v == nil { 42 | return &multiContext{} 43 | } 44 | 45 | return v.(*multiContext) 46 | } 47 | 48 | func releaseMultiContext(value *multiContext) { 49 | if value != nil { 50 | value.reset() 51 | multiContextPool.Put(value) 52 | } 53 | } 54 | 55 | func acquireDispathNode() *dispatchNode { 56 | v := dispatchNodePool.Get() 57 | if v == nil { 58 | return &dispatchNode{} 59 | } 60 | 61 | return v.(*dispatchNode) 62 | } 63 | 64 | func releaseDispathNode(value *dispatchNode) { 65 | if value != nil { 66 | value.reset() 67 | dispatchNodePool.Put(value) 68 | } 69 | } 70 | 71 | func acquireContext() *proxyContext { 72 | v := contextPool.Get() 73 | if v == nil { 74 | return &proxyContext{} 75 | } 76 | 77 | return v.(*proxyContext) 78 | } 79 | 80 | func releaseContext(value *proxyContext) { 81 | if value != nil { 82 | value.reset() 83 | contextPool.Put(value) 84 | } 85 | } 86 | 87 | func acquireRender() *render { 88 | v := renderPool.Get() 89 | if v == nil { 90 | return &render{} 91 | } 92 | 93 | return v.(*render) 94 | } 95 | 96 | func releaseRender(value *render) { 97 | if value != nil { 98 | value.reset() 99 | renderPool.Put(value) 100 | } 101 | } 102 | 103 | func acquireExprCtx() *expr.Ctx { 104 | v := exprCtxPool.Get() 105 | if v == nil { 106 | return &expr.Ctx{ 107 | Params: make(map[string][]byte), 108 | } 109 | } 110 | 111 | return v.(*expr.Ctx) 112 | } 113 | 114 | func releaseExprCtx(value *expr.Ctx) { 115 | if value != nil { 116 | value.Reset() 117 | exprCtxPool.Put(value) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pkg/proxy/proxy_gc.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/fagongzi/gateway/pkg/plugin" 8 | "github.com/fagongzi/log" 9 | ) 10 | 11 | func (p *Proxy) addGCJSEngine(value *plugin.Engine) { 12 | p.Lock() 13 | defer p.Unlock() 14 | 15 | p.gcJSEngines = append(p.gcJSEngines, value) 16 | } 17 | 18 | func (p *Proxy) readyToGCJSEngine() { 19 | _, err := p.runner.RunCancelableTask(func(ctx context.Context) { 20 | t := time.NewTicker(time.Minute) 21 | defer t.Stop() 22 | 23 | for { 24 | select { 25 | case <-ctx.Done(): 26 | log.Info("stop: gc js engine stopped") 27 | t.Stop() 28 | return 29 | case <-t.C: 30 | now := time.Now() 31 | p.Lock() 32 | var values []*plugin.Engine 33 | for _, eng := range p.gcJSEngines { 34 | if now.Sub(eng.LastActive()) > time.Hour { 35 | go eng.Destroy() 36 | } else { 37 | values = append(values, eng) 38 | } 39 | } 40 | p.gcJSEngines = values 41 | p.Unlock() 42 | } 43 | } 44 | }) 45 | if err != nil { 46 | log.Fatalf("start gc js engine failed, errors:\n%+v", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/proxy/proxy_https.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "crypto/tls" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/fagongzi/gateway/pkg/pb/metapb" 9 | "github.com/fagongzi/log" 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | func (p *Proxy) enableHTTPS() bool { 14 | return p.cfg.DefaultTLSCert != "" && p.cfg.DefaultTLSKey != "" && p.cfg.AddrHTTPS != "" 15 | } 16 | 17 | func (p *Proxy) appendCertsEmbed(server *fasthttp.Server, certData []byte, keyData []byte) { 18 | for _, api := range p.dispatcher.apis { 19 | if metapb.Up == api.meta.GetStatus() && api.meta.GetUseTLS() { 20 | server.AppendCertEmbed(api.meta.TlsEmbedCert.CertData, api.meta.TlsEmbedCert.KeyData) 21 | } 22 | } 23 | server.AppendCertEmbed(certData, keyData) 24 | } 25 | 26 | func (p *Proxy) configTLSConfig(server *http.Server, certData []byte, keyData []byte) { 27 | certs := make([]tls.Certificate, 0) 28 | for _, api := range p.dispatcher.apis { 29 | if metapb.Up == api.meta.GetStatus() && api.meta.GetUseTLS() { 30 | cert, err := tls.X509KeyPair(api.meta.TlsEmbedCert.CertData, api.meta.TlsEmbedCert.KeyData) 31 | if err != nil { 32 | log.Errorf("api %s has invalid TLS certs", api.meta.Name) 33 | continue 34 | } 35 | certs = append(certs, cert) 36 | } 37 | } 38 | cert, _ := tls.X509KeyPair(certData, keyData) 39 | certs = append(certs, cert) 40 | server.TLSConfig.Certificates = certs 41 | } 42 | 43 | func (p *Proxy) mustParseDefaultTLSCert() ([]byte, []byte) { 44 | certData, err := ioutil.ReadFile(p.cfg.DefaultTLSCert) 45 | if err != nil { 46 | log.Fatalf("parse https cert failed with %+v", err) 47 | } 48 | keyData, err := ioutil.ReadFile(p.cfg.DefaultTLSKey) 49 | if err != nil { 50 | log.Fatalf("parse https cert failed with %+v", err) 51 | } 52 | _, err = tls.X509KeyPair(certData, keyData) 53 | if err != nil { 54 | log.Fatalf("parse https cert failed with %+v", err) 55 | } 56 | return certData, keyData 57 | } 58 | -------------------------------------------------------------------------------- /pkg/proxy/proxy_websocket.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/fagongzi/log" 10 | "github.com/fagongzi/util/hack" 11 | "github.com/gorilla/websocket" 12 | "github.com/koding/websocketproxy" 13 | "github.com/valyala/fasthttp" 14 | ) 15 | 16 | const ( 17 | websocketRspKey = "__ws_rsp" 18 | ) 19 | var wsHeaders = map[string]bool{ 20 | "Origin": true, 21 | "Sec-WebSocket-Protocol": true, 22 | "Sec-Websocket-Protocol": true, 23 | "Cookie": true, 24 | "Sec-WebSocket-Version": true, 25 | "Sec-Websocket-Version": true, 26 | "Sec-WebSocket-Key": true, 27 | "Sec-Websocket-Key": true, 28 | "Sec-Websocket-Extensions": true, 29 | "Connection": true, 30 | "Upgrade": true, 31 | "Set-Cookie": true, 32 | "Sec-WebSocket-Extensions": true, 33 | "Sec-WebSocket-Accept": true, 34 | } 35 | // ServeHTTP http reverse handler by http 36 | func (p *Proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 37 | if p.isStopped() { 38 | rw.WriteHeader(fasthttp.StatusServiceUnavailable) 39 | return 40 | } 41 | 42 | var buf bytes.Buffer 43 | buf.WriteByte(charLeft) 44 | buf.Write(hack.StringToSlice(req.Method)) 45 | buf.WriteByte(charRight) 46 | buf.Write(hack.StringToSlice(req.RequestURI)) 47 | requestTag := hack.SliceToString(buf.Bytes()) 48 | 49 | if req.Method != "GET" { 50 | rw.WriteHeader(fasthttp.StatusMethodNotAllowed) 51 | return 52 | } 53 | 54 | ctx := &fasthttp.RequestCtx{} 55 | for k, vs := range req.Header { 56 | for _, v := range vs { 57 | ctx.Request.Header.Add(k, v) 58 | } 59 | } 60 | ctx.Request.SetRequestURI(req.RequestURI) 61 | 62 | api, dispatches, exprCtx := p.dispatcher.dispatch(ctx, requestTag) 63 | if len(dispatches) <= 0 && 64 | (nil == api || api.meta.DefaultValue == nil) { 65 | rw.WriteHeader(fasthttp.StatusNotFound) 66 | releaseExprCtx(exprCtx) 67 | return 68 | } 69 | 70 | if len(dispatches) != 1 { 71 | log.Fatalf("websocket not support dispatch to multi backend server") 72 | } 73 | 74 | if !api.isWebSocket() { 75 | log.Fatalf("normal http request must use fasthttp") 76 | } 77 | 78 | dispatches[0].ctx = ctx 79 | p.doProxy(dispatches[0], func(c *proxyContext) { 80 | c.SetAttr(websocketRspKey, rw) 81 | }) 82 | dispatches[0].release() 83 | releaseExprCtx(exprCtx) 84 | } 85 | 86 | func (p *Proxy) onWebsocket(c *proxyContext, addr string) (*fasthttp.Response, error) { 87 | resp := fasthttp.AcquireResponse() 88 | 89 | var r http.Request 90 | r.Method = "GET" 91 | r.Proto = "HTTP/1.1" 92 | r.ProtoMajor = 1 93 | r.ProtoMinor = 1 94 | r.RequestURI = string(c.forwardReq.RequestURI()) 95 | r.Host = string(c.forwardReq.Host()) 96 | 97 | hdr := make(http.Header) 98 | c.forwardReq.Header.VisitAll(func(k, v []byte) { 99 | sk := string(k) 100 | sv := string(v) 101 | switch sk { 102 | case "Transfer-Encoding": 103 | r.TransferEncoding = append(r.TransferEncoding, sv) 104 | default: 105 | hdr.Set(sk, sv) 106 | } 107 | }) 108 | r.Header = hdr 109 | r.URL, _ = url.ParseRequestURI(r.RequestURI) 110 | 111 | wp := &websocketproxy.WebsocketProxy{ 112 | Upgrader: &websocket.Upgrader{ 113 | ReadBufferSize: c.result.httpOption().ReadBufferSize, 114 | WriteBufferSize: c.result.httpOption().WriteBufferSize, 115 | CheckOrigin: func(r *http.Request) bool { 116 | return true 117 | }, 118 | }, 119 | Director: func(incoming *http.Request, out http.Header) { 120 | out.Set("Origin", fmt.Sprintf("http://%s", addr)) 121 | for key, vals := range incoming.Header { 122 | if _, ok := wsHeaders[key]; ok { 123 | continue 124 | } 125 | for _, val := range vals { 126 | out.Set(key, val) 127 | 128 | } 129 | } 130 | }, 131 | Backend: func(r *http.Request) *url.URL { 132 | u, _ := url.Parse(fmt.Sprintf("ws://%s%s", addr, r.RequestURI)) 133 | return u 134 | }, 135 | } 136 | 137 | wp.ServeHTTP(c.GetAttr(websocketRspKey).(http.ResponseWriter), &r) 138 | return resp, nil 139 | } 140 | -------------------------------------------------------------------------------- /pkg/proxy/rate_limit.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/fagongzi/gateway/pkg/pb/metapb" 7 | "github.com/juju/ratelimit" 8 | ) 9 | 10 | type rateLimiter struct { 11 | limiter *ratelimit.Bucket 12 | option metapb.RateLimitOption 13 | } 14 | 15 | func newRateLimiter(max int64, option metapb.RateLimitOption) *rateLimiter { 16 | return &rateLimiter{ 17 | limiter: ratelimit.NewBucket(time.Second/time.Duration(max), max), 18 | option: option, 19 | } 20 | } 21 | 22 | func (l *rateLimiter) do(count int64) bool { 23 | if l.option == metapb.Wait { 24 | l.limiter.Wait(count) 25 | return true 26 | } 27 | 28 | return l.limiter.TakeAvailable(count) > 0 29 | } 30 | -------------------------------------------------------------------------------- /pkg/route/const.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | const ( 4 | eoi byte = 0x1A 5 | slash = byte('/') 6 | lParen = byte('(') 7 | rParen = byte(')') 8 | vertical = byte('|') 9 | colon = byte(':') 10 | ) 11 | 12 | const ( 13 | tokenEOF = iota 14 | tokenUnknown 15 | tokenSlash 16 | tokenLParen 17 | tokenRParen 18 | tokenVertical 19 | tokenColon 20 | ) 21 | 22 | var ( 23 | slashValue = []byte("/") 24 | numberValue = []byte("number") 25 | stringValue = []byte("string") 26 | enumValue = []byte("enum") 27 | 28 | matchAll = []byte("*") 29 | ) 30 | 31 | type nodeType int 32 | 33 | const ( 34 | slashType = nodeType(5) 35 | stringType = nodeType(4) 36 | constType = nodeType(3) 37 | enumType = nodeType(2) 38 | numberType = nodeType(1) 39 | ) 40 | -------------------------------------------------------------------------------- /pkg/route/lexer.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | type lexer interface { 4 | Next() byte 5 | Current() byte 6 | NextToken() 7 | Token() int 8 | TokenIndex() int 9 | ScanString() []byte 10 | } 11 | -------------------------------------------------------------------------------- /pkg/route/scanner.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | type scanner struct { 4 | len int 5 | input []byte 6 | 7 | token int 8 | bp int 9 | sp int 10 | ch byte 11 | } 12 | 13 | func newScanner(input []byte) lexer { 14 | scan := &scanner{ 15 | len: len(input), 16 | input: input, 17 | bp: -1, 18 | sp: 0, 19 | } 20 | 21 | scan.Next() 22 | return scan 23 | } 24 | 25 | func (scan *scanner) Next() byte { 26 | scan.bp++ 27 | 28 | if scan.bp < scan.len { 29 | scan.ch = scan.input[scan.bp] 30 | } else { 31 | scan.ch = eoi 32 | } 33 | 34 | return scan.ch 35 | } 36 | 37 | func (scan *scanner) NextToken() { 38 | for { 39 | switch scan.ch { 40 | case '/': 41 | scan.token = tokenSlash 42 | scan.Next() 43 | return 44 | case '(': 45 | scan.token = tokenLParen 46 | scan.Next() 47 | return 48 | case '|': 49 | scan.token = tokenVertical 50 | scan.Next() 51 | return 52 | case ':': 53 | scan.token = tokenColon 54 | scan.Next() 55 | return 56 | case ')': 57 | scan.token = tokenRParen 58 | scan.Next() 59 | return 60 | case eoi: 61 | scan.token = tokenEOF 62 | scan.Next() 63 | return 64 | } 65 | 66 | scan.Next() 67 | } 68 | } 69 | 70 | func (scan *scanner) Current() byte { 71 | return scan.ch 72 | } 73 | 74 | func (scan *scanner) Token() int { 75 | return scan.token 76 | } 77 | 78 | func (scan *scanner) TokenIndex() int { 79 | return scan.bp - 1 80 | } 81 | 82 | func (scan *scanner) ScanString() []byte { 83 | value := scan.input[scan.sp : scan.bp-1] 84 | scan.sp = scan.bp 85 | return value 86 | } 87 | -------------------------------------------------------------------------------- /pkg/route/scanner_test.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNext(t *testing.T) { 8 | input := []byte("0") 9 | s := newScanner(input) 10 | 11 | ch := s.Current() 12 | if ch != '0' { 13 | t.Errorf("ch expect 0 but %c", ch) 14 | } 15 | 16 | s.Next() 17 | ch = s.Current() 18 | if ch != eoi { 19 | t.Errorf("ch expect eoi but %c", ch) 20 | } 21 | } 22 | 23 | func TestNextTokenAndScanString(t *testing.T) { 24 | input := []byte("0/1(2|3:4)") 25 | s := newScanner(input) 26 | 27 | s.NextToken() 28 | token := s.Token() 29 | if token != tokenSlash { 30 | t.Errorf("token expect / but %d", token) 31 | } 32 | value := string(s.ScanString()) 33 | if value != "0" { 34 | t.Errorf("scanstring expect 0 but %s", value) 35 | } 36 | 37 | s.NextToken() 38 | token = s.Token() 39 | if token != tokenLParen { 40 | t.Errorf("token expect ( but %d", token) 41 | } 42 | value = string(s.ScanString()) 43 | if value != "1" { 44 | t.Errorf("scanstring expect 1 but %s", value) 45 | } 46 | 47 | s.NextToken() 48 | token = s.Token() 49 | if token != tokenVertical { 50 | t.Errorf("token expect | but %d", token) 51 | } 52 | value = string(s.ScanString()) 53 | if value != "2" { 54 | t.Errorf("scanstring expect 2 but %s", value) 55 | } 56 | 57 | s.NextToken() 58 | token = s.Token() 59 | if token != tokenColon { 60 | t.Errorf("token expect : but %d", token) 61 | } 62 | value = string(s.ScanString()) 63 | if value != "3" { 64 | t.Errorf("scanstring expect 3 but %s", value) 65 | } 66 | 67 | s.NextToken() 68 | token = s.Token() 69 | if token != tokenRParen { 70 | t.Errorf("token expect ) but %d", token) 71 | } 72 | value = string(s.ScanString()) 73 | if value != "4" { 74 | t.Errorf("scanstring expect 4 but %s", value) 75 | } 76 | 77 | s.NextToken() 78 | token = s.Token() 79 | if token != tokenEOF { 80 | t.Errorf("token expect eof but %d", token) 81 | } 82 | value = string(s.ScanString()) 83 | if value != "" { 84 | t.Errorf("scanstring expect empty but %s", value) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkg/service/errors.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | errRPCCancel = errors.New("rpc cancel") 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/service/g.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/rpcpb" 5 | "github.com/fagongzi/gateway/pkg/store" 6 | ) 7 | 8 | var ( 9 | // MetaService global service 10 | MetaService rpcpb.MetaServiceServer 11 | // Store global store db 12 | Store store.Store 13 | ) 14 | 15 | // Init init service package 16 | func Init(db store.Store) { 17 | Store = db 18 | MetaService = newMetaService(db) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/service/http.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fagongzi/util/format" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | const ( 11 | apiVersion = "/v1" 12 | ) 13 | 14 | // InitHTTPRouter init http router 15 | func InitHTTPRouter(server *echo.Echo, ui, uiPrefix string) { 16 | versionGroup := server.Group(apiVersion) 17 | initClusterRouter(versionGroup) 18 | initServerRouter(versionGroup) 19 | initBindRouter(versionGroup) 20 | initRoutingRouter(versionGroup) 21 | initAPIRouter(versionGroup) 22 | initPluginRouter(versionGroup) 23 | initSystemRouter(versionGroup) 24 | initStatic(server, ui, uiPrefix) 25 | } 26 | 27 | type limitQuery struct { 28 | limit int64 29 | afterID uint64 30 | } 31 | 32 | func idParamFactory(ctx echo.Context) (interface{}, error) { 33 | value := ctx.Param("id") 34 | if value == "" { 35 | return nil, fmt.Errorf("missing id path value") 36 | } 37 | 38 | id, err := format.ParseStrUInt64(value) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return id, nil 44 | } 45 | 46 | func limitQueryFactory(ctx echo.Context) (interface{}, error) { 47 | query := &limitQuery{ 48 | limit: limit, 49 | } 50 | 51 | value := ctx.QueryParam("limit") 52 | if value != "" { 53 | l, err := format.ParseStrInt64(value) 54 | if err != nil { 55 | return nil, err 56 | } 57 | query.limit = l 58 | } 59 | 60 | value = ctx.QueryParam("after") 61 | if value != "" { 62 | l, err := format.ParseStrUInt64(value) 63 | if err != nil { 64 | return nil, err 65 | } 66 | query.afterID = l 67 | } 68 | 69 | return query, nil 70 | } 71 | 72 | func emptyParamFactory(ctx echo.Context) (interface{}, error) { 73 | return nil, nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/service/http_api.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/fagongzi/grpcx" 6 | "github.com/fagongzi/log" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | func initAPIRouter(server *echo.Group) { 11 | server.GET("/apis/:id", 12 | grpcx.NewGetHTTPHandle(idParamFactory, getAPIHandler)) 13 | server.DELETE("/apis/:id", 14 | grpcx.NewGetHTTPHandle(idParamFactory, deleteAPIHandler)) 15 | server.PUT("/apis", 16 | grpcx.NewJSONBodyHTTPHandle(putAPIFactory, postAPIHandler)) 17 | server.GET("/apis", 18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listAPIHandler)) 19 | } 20 | 21 | func postAPIHandler(value interface{}) (*grpcx.JSONResult, error) { 22 | id, err := Store.PutAPI(value.(*metapb.API)) 23 | if err != nil { 24 | log.Errorf("api-api-put: req %+v, errors:%+v", value, err) 25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 26 | } 27 | 28 | return &grpcx.JSONResult{Data: id}, nil 29 | } 30 | 31 | func deleteAPIHandler(value interface{}) (*grpcx.JSONResult, error) { 32 | err := Store.RemoveAPI(value.(uint64)) 33 | if err != nil { 34 | log.Errorf("api-api-delete: req %+v, errors:%+v", value, err) 35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 36 | } 37 | 38 | return &grpcx.JSONResult{}, nil 39 | } 40 | 41 | func getAPIHandler(value interface{}) (*grpcx.JSONResult, error) { 42 | value, err := Store.GetAPI(value.(uint64)) 43 | if err != nil { 44 | log.Errorf("api-api-get: req %+v, errors:%+v", value, err) 45 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 46 | } 47 | 48 | return &grpcx.JSONResult{Data: value}, nil 49 | } 50 | 51 | func listAPIHandler(value interface{}) (*grpcx.JSONResult, error) { 52 | query := value.(*limitQuery) 53 | var values []*metapb.API 54 | 55 | err := Store.GetAPIs(limit, func(data interface{}) error { 56 | v := data.(*metapb.API) 57 | if int64(len(values)) < query.limit && v.ID > query.afterID { 58 | values = append(values, v) 59 | } 60 | return nil 61 | }) 62 | if err != nil { 63 | log.Errorf("api-api-list-get: req %+v, errors:%+v", value, err) 64 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 65 | } 66 | 67 | return &grpcx.JSONResult{Data: values}, nil 68 | } 69 | 70 | func putAPIFactory() interface{} { 71 | return &metapb.API{} 72 | } 73 | -------------------------------------------------------------------------------- /pkg/service/http_bind.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/fagongzi/grpcx" 6 | "github.com/fagongzi/log" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | func initBindRouter(server *echo.Group) { 11 | server.DELETE("/binds", 12 | grpcx.NewJSONBodyHTTPHandle(bindFactory, deleteBindHandler)) 13 | 14 | server.PUT("/binds", 15 | grpcx.NewJSONBodyHTTPHandle(bindFactory, postBindHandler)) 16 | } 17 | 18 | func postBindHandler(value interface{}) (*grpcx.JSONResult, error) { 19 | err := Store.AddBind(value.(*metapb.Bind)) 20 | if err != nil { 21 | log.Errorf("api-bind-put: req %+v, errors:%+v", value, err) 22 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 23 | } 24 | 25 | return &grpcx.JSONResult{}, nil 26 | } 27 | 28 | func deleteBindHandler(value interface{}) (*grpcx.JSONResult, error) { 29 | err := Store.RemoveBind(value.(*metapb.Bind)) 30 | if err != nil { 31 | log.Errorf("api-bind-delete: req %+v, errors:%+v", value, err) 32 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 33 | } 34 | 35 | return &grpcx.JSONResult{}, nil 36 | } 37 | 38 | func bindFactory() interface{} { 39 | return &metapb.Bind{} 40 | } 41 | -------------------------------------------------------------------------------- /pkg/service/http_cluster.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/fagongzi/grpcx" 6 | "github.com/fagongzi/log" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | func initClusterRouter(server *echo.Group) { 11 | server.GET("/clusters/:id", 12 | grpcx.NewGetHTTPHandle(idParamFactory, getClusterHandler)) 13 | server.GET("/clusters/:id/binds", 14 | grpcx.NewGetHTTPHandle(idParamFactory, bindsClusterHandler)) 15 | server.DELETE("/clusters/:id", 16 | grpcx.NewGetHTTPHandle(idParamFactory, deleteClusterHandler)) 17 | server.DELETE("/clusters/:id/binds", 18 | grpcx.NewGetHTTPHandle(idParamFactory, deleteClusterBindsHandler)) 19 | server.PUT("/clusters", 20 | grpcx.NewJSONBodyHTTPHandle(putClusterFactory, postClusterHandler)) 21 | server.GET("/clusters", 22 | grpcx.NewGetHTTPHandle(limitQueryFactory, listClusterHandler)) 23 | } 24 | 25 | func postClusterHandler(value interface{}) (*grpcx.JSONResult, error) { 26 | id, err := Store.PutCluster(value.(*metapb.Cluster)) 27 | if err != nil { 28 | log.Errorf("api-cluster-put: req %+v, errors:%+v", value, err) 29 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 30 | } 31 | 32 | return &grpcx.JSONResult{Data: id}, nil 33 | } 34 | 35 | func deleteClusterHandler(value interface{}) (*grpcx.JSONResult, error) { 36 | err := Store.RemoveCluster(value.(uint64)) 37 | if err != nil { 38 | log.Errorf("api-cluster-delete: req %+v, errors:%+v", value, err) 39 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 40 | } 41 | 42 | return &grpcx.JSONResult{}, nil 43 | } 44 | 45 | func deleteClusterBindsHandler(value interface{}) (*grpcx.JSONResult, error) { 46 | err := Store.RemoveClusterBind(value.(uint64)) 47 | if err != nil { 48 | log.Errorf("api-cluster-binds-delete: req %+v, errors:%+v", value, err) 49 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 50 | } 51 | 52 | return &grpcx.JSONResult{}, nil 53 | } 54 | 55 | func getClusterHandler(value interface{}) (*grpcx.JSONResult, error) { 56 | value, err := Store.GetCluster(value.(uint64)) 57 | if err != nil { 58 | log.Errorf("api-cluster-get: req %+v, errors:%+v", value, err) 59 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 60 | } 61 | 62 | return &grpcx.JSONResult{Data: value}, nil 63 | } 64 | 65 | func bindsClusterHandler(value interface{}) (*grpcx.JSONResult, error) { 66 | values, err := Store.GetBindServers(value.(uint64)) 67 | if err != nil { 68 | log.Errorf("api-cluster-binds-get: req %+v, errors:%+v", value, err) 69 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 70 | } 71 | 72 | return &grpcx.JSONResult{Data: values}, nil 73 | } 74 | 75 | func listClusterHandler(value interface{}) (*grpcx.JSONResult, error) { 76 | query := value.(*limitQuery) 77 | var values []*metapb.Cluster 78 | 79 | err := Store.GetClusters(limit, func(data interface{}) error { 80 | v := data.(*metapb.Cluster) 81 | if int64(len(values)) < query.limit && v.ID > query.afterID { 82 | values = append(values, v) 83 | } 84 | return nil 85 | }) 86 | if err != nil { 87 | log.Errorf("api-cluster-list-get: req %+v, errors:%+v", value, err) 88 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 89 | } 90 | 91 | return &grpcx.JSONResult{Data: values}, nil 92 | } 93 | 94 | func putClusterFactory() interface{} { 95 | return &metapb.Cluster{} 96 | } 97 | -------------------------------------------------------------------------------- /pkg/service/http_plugin.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/fagongzi/grpcx" 6 | "github.com/fagongzi/log" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | func initPluginRouter(server *echo.Group) { 11 | server.GET("/plugins/:id", 12 | grpcx.NewGetHTTPHandle(idParamFactory, getPluginHandler)) 13 | server.DELETE("/plugins/:id", 14 | grpcx.NewGetHTTPHandle(idParamFactory, deletePluginHandler)) 15 | server.PUT("/plugins", 16 | grpcx.NewJSONBodyHTTPHandle(putPluginFactory, postPluginHandler)) 17 | server.GET("/plugins", 18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listPluginHandler)) 19 | server.PUT("/plugins/apply", 20 | grpcx.NewJSONBodyHTTPHandle(putPluginAppliedFactory, putPluginAppliedHandler)) 21 | server.GET("/plugins/apply", 22 | grpcx.NewGetHTTPHandle(emptyParamFactory, getPluginAppliedHandler)) 23 | } 24 | 25 | func getPluginAppliedHandler(value interface{}) (*grpcx.JSONResult, error) { 26 | value, err := Store.GetAppliedPlugins() 27 | if err != nil { 28 | log.Errorf("api-plugin-get-applied: req %+v, errors:%+v", value, err) 29 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 30 | } 31 | 32 | return &grpcx.JSONResult{Data: value}, nil 33 | } 34 | 35 | func putPluginAppliedHandler(value interface{}) (*grpcx.JSONResult, error) { 36 | err := Store.ApplyPlugins(value.(*metapb.AppliedPlugins)) 37 | if err != nil { 38 | log.Errorf("api-plugin-put-applied: req %+v, errors:%+v", value, err) 39 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 40 | } 41 | 42 | return &grpcx.JSONResult{}, nil 43 | } 44 | 45 | func postPluginHandler(value interface{}) (*grpcx.JSONResult, error) { 46 | id, err := Store.PutPlugin(value.(*metapb.Plugin)) 47 | if err != nil { 48 | log.Errorf("api-plugin-put: req %+v, errors:%+v", value, err) 49 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 50 | } 51 | 52 | return &grpcx.JSONResult{Data: id}, nil 53 | } 54 | 55 | func deletePluginHandler(value interface{}) (*grpcx.JSONResult, error) { 56 | err := Store.RemovePlugin(value.(uint64)) 57 | if err != nil { 58 | log.Errorf("api-plugin-delete: req %+v, errors:%+v", value, err) 59 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 60 | } 61 | 62 | return &grpcx.JSONResult{}, nil 63 | } 64 | 65 | func getPluginHandler(value interface{}) (*grpcx.JSONResult, error) { 66 | value, err := Store.GetPlugin(value.(uint64)) 67 | if err != nil { 68 | log.Errorf("api-plugin-get: req %+v, errors:%+v", value, err) 69 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 70 | } 71 | 72 | return &grpcx.JSONResult{Data: value}, nil 73 | } 74 | 75 | func listPluginHandler(value interface{}) (*grpcx.JSONResult, error) { 76 | query := value.(*limitQuery) 77 | var values []*metapb.Plugin 78 | 79 | err := Store.GetPlugins(limit, func(data interface{}) error { 80 | v := data.(*metapb.Plugin) 81 | if int64(len(values)) < query.limit && v.ID > query.afterID { 82 | values = append(values, v) 83 | } 84 | return nil 85 | }) 86 | if err != nil { 87 | log.Errorf("api-plugin-list-get: req %+v, errors:%+v", value, err) 88 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 89 | } 90 | 91 | return &grpcx.JSONResult{Data: values}, nil 92 | } 93 | 94 | func putPluginFactory() interface{} { 95 | return &metapb.Plugin{} 96 | } 97 | 98 | func putPluginAppliedFactory() interface{} { 99 | return &metapb.AppliedPlugins{} 100 | } 101 | -------------------------------------------------------------------------------- /pkg/service/http_routing.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/fagongzi/grpcx" 6 | "github.com/fagongzi/log" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | func initRoutingRouter(server *echo.Group) { 11 | server.GET("/routings/:id", 12 | grpcx.NewGetHTTPHandle(idParamFactory, getRoutingHandler)) 13 | server.DELETE("/routings/:id", 14 | grpcx.NewGetHTTPHandle(idParamFactory, deleteRoutingHandler)) 15 | server.PUT("/routings", 16 | grpcx.NewJSONBodyHTTPHandle(putRoutingFactory, postRoutingHandler)) 17 | server.GET("/routings", 18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listRoutingHandler)) 19 | } 20 | 21 | func postRoutingHandler(value interface{}) (*grpcx.JSONResult, error) { 22 | id, err := Store.PutRouting(value.(*metapb.Routing)) 23 | if err != nil { 24 | log.Errorf("api-routing-put: req %+v, errors:%+v", value, err) 25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 26 | } 27 | 28 | return &grpcx.JSONResult{Data: id}, nil 29 | } 30 | 31 | func deleteRoutingHandler(value interface{}) (*grpcx.JSONResult, error) { 32 | err := Store.RemoveRouting(value.(uint64)) 33 | if err != nil { 34 | log.Errorf("api-routing-delete: req %+v, errors:%+v", value, err) 35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 36 | } 37 | 38 | return &grpcx.JSONResult{}, nil 39 | } 40 | 41 | func getRoutingHandler(value interface{}) (*grpcx.JSONResult, error) { 42 | value, err := Store.GetRouting(value.(uint64)) 43 | if err != nil { 44 | log.Errorf("api-routing-delete: req %+v, errors:%+v", value, err) 45 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 46 | } 47 | 48 | return &grpcx.JSONResult{Data: value}, nil 49 | } 50 | 51 | func putRoutingFactory() interface{} { 52 | return &metapb.Routing{} 53 | } 54 | 55 | func listRoutingHandler(value interface{}) (*grpcx.JSONResult, error) { 56 | query := value.(*limitQuery) 57 | var values []*metapb.Routing 58 | 59 | err := Store.GetRoutings(limit, func(data interface{}) error { 60 | v := data.(*metapb.Routing) 61 | if int64(len(values)) < query.limit && v.ID > query.afterID { 62 | values = append(values, v) 63 | } 64 | return nil 65 | }) 66 | if err != nil { 67 | log.Errorf("api-routing-list-get: req %+v, errors:%+v", value, err) 68 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 69 | } 70 | 71 | return &grpcx.JSONResult{Data: values}, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/service/http_server.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/gateway/pkg/pb/metapb" 5 | "github.com/fagongzi/grpcx" 6 | "github.com/fagongzi/log" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | func initServerRouter(server *echo.Group) { 11 | server.GET("/servers/:id", 12 | grpcx.NewGetHTTPHandle(idParamFactory, getServerHandler)) 13 | server.DELETE("/servers/:id", 14 | grpcx.NewGetHTTPHandle(idParamFactory, deleteServerHandler)) 15 | server.PUT("/servers", 16 | grpcx.NewJSONBodyHTTPHandle(putServerFactory, postServerHandler)) 17 | server.GET("/servers", 18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listServerHandler)) 19 | } 20 | 21 | func postServerHandler(value interface{}) (*grpcx.JSONResult, error) { 22 | id, err := Store.PutServer(value.(*metapb.Server)) 23 | if err != nil { 24 | log.Errorf("api-server-put: req %+v, errors:%+v", value, err) 25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 26 | } 27 | 28 | return &grpcx.JSONResult{Data: id}, nil 29 | } 30 | 31 | func deleteServerHandler(value interface{}) (*grpcx.JSONResult, error) { 32 | err := Store.RemoveServer(value.(uint64)) 33 | if err != nil { 34 | log.Errorf("api-server-delete: req %+v, errors:%+v", value, err) 35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 36 | } 37 | 38 | return &grpcx.JSONResult{}, nil 39 | } 40 | 41 | func getServerHandler(value interface{}) (*grpcx.JSONResult, error) { 42 | value, err := Store.GetServer(value.(uint64)) 43 | if err != nil { 44 | log.Errorf("api-server-get: req %+v, errors:%+v", value, err) 45 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 46 | } 47 | 48 | return &grpcx.JSONResult{Data: value}, nil 49 | } 50 | 51 | func listServerHandler(value interface{}) (*grpcx.JSONResult, error) { 52 | query := value.(*limitQuery) 53 | var values []*metapb.Server 54 | 55 | err := Store.GetServers(limit, func(data interface{}) error { 56 | v := data.(*metapb.Server) 57 | if int64(len(values)) < query.limit && v.ID > query.afterID { 58 | values = append(values, v) 59 | } 60 | return nil 61 | }) 62 | if err != nil { 63 | log.Errorf("api-server-list-get: req %+v, errors:%+v", value, err) 64 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 65 | } 66 | 67 | return &grpcx.JSONResult{Data: values}, nil 68 | } 69 | 70 | func putServerFactory() interface{} { 71 | return &metapb.Server{} 72 | } 73 | -------------------------------------------------------------------------------- /pkg/service/http_static.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | func initStatic(server *echo.Echo, ui, uiPrefix string) { 10 | server.Static(uiPrefix, ui) 11 | server.Static("static", fmt.Sprintf("%s/static", ui)) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/service/http_system.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/fagongzi/grpcx" 5 | "github.com/fagongzi/log" 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | type backup struct { 10 | ToAddr string `json:"toAddr"` 11 | } 12 | 13 | func initSystemRouter(server *echo.Group) { 14 | server.GET("/system", 15 | grpcx.NewGetHTTPHandle(emptyParamFactory, getSystemHandler)) 16 | 17 | server.POST("/system/backup", 18 | grpcx.NewJSONBodyHTTPHandle(backupFactory, postBackupHandler)) 19 | } 20 | 21 | func getSystemHandler(value interface{}) (*grpcx.JSONResult, error) { 22 | info, err := Store.System() 23 | if err != nil { 24 | log.Errorf("api-system-get: errors:%+v", err) 25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 26 | } 27 | 28 | return &grpcx.JSONResult{Data: info}, nil 29 | } 30 | 31 | func postBackupHandler(value interface{}) (*grpcx.JSONResult, error) { 32 | err := Store.BackupTo(value.(*backup).ToAddr) 33 | if err != nil { 34 | log.Errorf("api-system-backup: errors:%+v", err) 35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil 36 | } 37 | 38 | return &grpcx.JSONResult{}, nil 39 | } 40 | 41 | func backupFactory() interface{} { 42 | return &backup{} 43 | } 44 | -------------------------------------------------------------------------------- /pkg/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | "time" 8 | 9 | "github.com/fagongzi/gateway/pkg/pb/metapb" 10 | "github.com/fagongzi/gateway/pkg/pb/rpcpb" 11 | "github.com/fagongzi/gateway/pkg/util" 12 | ) 13 | 14 | var ( 15 | // TICKER ticket 16 | TICKER = time.Second * 3 17 | // TTL timeout 18 | TTL = int64(5) 19 | ) 20 | 21 | var ( 22 | supportSchema = make(map[string]func(string, string, BasicAuth) (Store, error)) 23 | ) 24 | 25 | // EvtType event type 26 | type EvtType int 27 | 28 | // EvtSrc event src 29 | type EvtSrc int 30 | 31 | // BasicAuth basic auth 32 | type BasicAuth struct { 33 | userName string 34 | password string 35 | } 36 | 37 | const ( 38 | // EventTypeNew event type new 39 | EventTypeNew = EvtType(0) 40 | // EventTypeUpdate event type update 41 | EventTypeUpdate = EvtType(1) 42 | // EventTypeDelete event type delete 43 | EventTypeDelete = EvtType(2) 44 | ) 45 | 46 | const ( 47 | // EventSrcCluster cluster event 48 | EventSrcCluster = EvtSrc(0) 49 | // EventSrcServer server event 50 | EventSrcServer = EvtSrc(1) 51 | // EventSrcBind bind event 52 | EventSrcBind = EvtSrc(2) 53 | // EventSrcAPI api event 54 | EventSrcAPI = EvtSrc(3) 55 | // EventSrcRouting routing event 56 | EventSrcRouting = EvtSrc(4) 57 | // EventSrcProxy routing event 58 | EventSrcProxy = EvtSrc(5) 59 | // EventSrcPlugin plugin event 60 | EventSrcPlugin = EvtSrc(6) 61 | // EventSrcApplyPlugin apply plugin event 62 | EventSrcApplyPlugin = EvtSrc(7) 63 | ) 64 | 65 | // Evt event 66 | type Evt struct { 67 | Src EvtSrc 68 | Type EvtType 69 | Key string 70 | Value interface{} 71 | } 72 | 73 | func init() { 74 | supportSchema["etcd"] = getEtcdStoreFrom 75 | } 76 | 77 | // GetStoreFrom returns a store implemention, if not support returns error 78 | func GetStoreFrom(registryAddr, prefix string, userName string, password string) (Store, error) { 79 | u, err := url.Parse(registryAddr) 80 | if err != nil { 81 | panic(fmt.Sprintf("parse registry addr failed, errors:%+v", err)) 82 | } 83 | 84 | schema := strings.ToLower(u.Scheme) 85 | fn, ok := supportSchema[schema] 86 | if ok { 87 | return fn(u.Host, prefix, BasicAuth{userName: userName, password: password}) 88 | } 89 | 90 | return nil, fmt.Errorf("not support: %s", registryAddr) 91 | } 92 | 93 | func getEtcdStoreFrom(addr, prefix string, basicAuth BasicAuth) (Store, error) { 94 | var addrs []string 95 | values := strings.Split(addr, ",") 96 | 97 | for _, value := range values { 98 | addrs = append(addrs, fmt.Sprintf("http://%s", value)) 99 | } 100 | 101 | return NewEtcdStore(addrs, prefix, basicAuth) 102 | } 103 | 104 | // Store store interface 105 | type Store interface { 106 | Raw() interface{} 107 | 108 | AddBind(bind *metapb.Bind) error 109 | RemoveBind(bind *metapb.Bind) error 110 | RemoveClusterBind(id uint64) error 111 | GetBindServers(id uint64) ([]uint64, error) 112 | 113 | PutCluster(cluster *metapb.Cluster) (uint64, error) 114 | RemoveCluster(id uint64) error 115 | GetClusters(limit int64, fn func(interface{}) error) error 116 | GetCluster(id uint64) (*metapb.Cluster, error) 117 | 118 | PutServer(svr *metapb.Server) (uint64, error) 119 | RemoveServer(id uint64) error 120 | GetServers(limit int64, fn func(interface{}) error) error 121 | GetServer(id uint64) (*metapb.Server, error) 122 | 123 | PutAPI(api *metapb.API) (uint64, error) 124 | RemoveAPI(id uint64) error 125 | GetAPIs(limit int64, fn func(interface{}) error) error 126 | GetAPI(id uint64) (*metapb.API, error) 127 | 128 | PutRouting(routing *metapb.Routing) (uint64, error) 129 | RemoveRouting(id uint64) error 130 | GetRoutings(limit int64, fn func(interface{}) error) error 131 | GetRouting(id uint64) (*metapb.Routing, error) 132 | 133 | PutPlugin(plugin *metapb.Plugin) (uint64, error) 134 | RemovePlugin(id uint64) error 135 | GetPlugins(limit int64, fn func(interface{}) error) error 136 | GetPlugin(id uint64) (*metapb.Plugin, error) 137 | ApplyPlugins(applied *metapb.AppliedPlugins) error 138 | GetAppliedPlugins() (*metapb.AppliedPlugins, error) 139 | 140 | RegistryProxy(proxy *metapb.Proxy, ttl int64) error 141 | GetProxies(limit int64, fn func(*metapb.Proxy) error) error 142 | 143 | Watch(evtCh chan *Evt, stopCh chan bool) error 144 | 145 | Clean() error 146 | SetID(id uint64) error 147 | BackupTo(to string) error 148 | Batch(batch *rpcpb.BatchReq) (*rpcpb.BatchRsp, error) 149 | System() (*metapb.System, error) 150 | } 151 | 152 | func getKey(prefix string, id uint64) string { 153 | return fmt.Sprintf("%s/%020d", prefix, id) 154 | } 155 | 156 | func getAddrKey(prefix string, addr string) string { 157 | return fmt.Sprintf("%s/%s", prefix, util.GetAddrFormat(addr)) 158 | } 159 | -------------------------------------------------------------------------------- /pkg/store/txn.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/coreos/etcd/clientv3" 7 | "github.com/fagongzi/log" 8 | "golang.org/x/net/context" 9 | ) 10 | 11 | // slowLogTxn wraps etcd transaction and log slow one. 12 | type slowLogTxn struct { 13 | clientv3.Txn 14 | cancel context.CancelFunc 15 | } 16 | 17 | func newSlowLogTxn(client *clientv3.Client) clientv3.Txn { 18 | ctx, cancel := context.WithTimeout(client.Ctx(), DefaultRequestTimeout) 19 | return &slowLogTxn{ 20 | Txn: client.Txn(ctx), 21 | cancel: cancel, 22 | } 23 | } 24 | 25 | func (t *slowLogTxn) If(cs ...clientv3.Cmp) clientv3.Txn { 26 | return &slowLogTxn{ 27 | Txn: t.Txn.If(cs...), 28 | cancel: t.cancel, 29 | } 30 | } 31 | 32 | func (t *slowLogTxn) Then(ops ...clientv3.Op) clientv3.Txn { 33 | return &slowLogTxn{ 34 | Txn: t.Txn.Then(ops...), 35 | cancel: t.cancel, 36 | } 37 | } 38 | 39 | // Commit implements Txn Commit interface. 40 | func (t *slowLogTxn) Commit() (*clientv3.TxnResponse, error) { 41 | start := time.Now() 42 | resp, err := t.Txn.Commit() 43 | t.cancel() 44 | 45 | cost := time.Now().Sub(start) 46 | if cost > DefaultSlowRequestTime { 47 | log.Warn("slow: txn runs too slow, resp=<%v> cost=<%s> errors:\n %+v", 48 | resp, 49 | cost, 50 | err) 51 | } 52 | 53 | return resp, err 54 | } 55 | 56 | func (e *EtcdStore) txn() clientv3.Txn { 57 | return newSlowLogTxn(e.rawClient) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/util/addr.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | // MinAddrFormat min addr format 9 | MinAddrFormat = "000000000000000000000" 10 | // MaxAddrFormat max addr format 11 | MaxAddrFormat = "255.255.255.255:99999" 12 | ) 13 | 14 | // GetAddrFormat returns addr format for sort, padding left by 0 15 | func GetAddrFormat(addr string) string { 16 | return fmt.Sprintf("%021s", addr) 17 | } 18 | 19 | // GetAddrNextFormat returns next addr format for sort, padding left by 0 20 | func GetAddrNextFormat(addr string) string { 21 | return fmt.Sprintf("%s%c", addr[:len(addr)-1], addr[len(addr)-1]+1) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/util/analysis_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/fagongzi/goetty" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func mlen(m *sync.Map) int { 14 | c := 0 15 | m.Range(func(key, value interface{}) bool { 16 | c++ 17 | return true 18 | }) 19 | 20 | return c 21 | } 22 | 23 | func TestAddTarget(t *testing.T) { 24 | key := uint64(1) 25 | tw := goetty.NewTimeoutWheel(goetty.WithTickInterval(time.Millisecond * 10)) 26 | ans := NewAnalysis(tw) 27 | ans.AddTarget(key, time.Millisecond*10) 28 | 29 | assert.Equal(t, 1, mlen(&ans.points), 30 | fmt.Sprintf("expect 1 points but %d", mlen(&ans.points))) 31 | 32 | assert.Equal(t, 1, mlen(&ans.recentlyPoints), 33 | fmt.Sprintf("expect 1 recently points but %d", mlen(&ans.recentlyPoints))) 34 | 35 | m, _ := ans.recentlyPoints.Load(key) 36 | assert.Equal(t, 1, mlen(m.(*sync.Map)), 37 | fmt.Sprintf("expect 1 recently points but %d", mlen(m.(*sync.Map)))) 38 | } 39 | 40 | func TestRemoveTarget(t *testing.T) { 41 | key := uint64(1) 42 | tw := goetty.NewTimeoutWheel(goetty.WithTickInterval(time.Millisecond * 10)) 43 | ans := NewAnalysis(tw) 44 | ans.AddTarget(key, time.Millisecond*10) 45 | ans.RemoveTarget(key) 46 | 47 | assert.Equal(t, 0, mlen(&ans.points), 48 | fmt.Sprintf("expect 0 points but %d", mlen(&ans.points))) 49 | 50 | assert.Equal(t, 0, mlen(&ans.recentlyPoints), 51 | fmt.Sprintf("expect 0 recently points but %d", mlen(&ans.recentlyPoints))) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/util/barrier.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | var ( 10 | lock sync.Mutex 11 | randSources = make(map[int][]int) 12 | ) 13 | 14 | func createRandSourceByBase(base int) []int { 15 | lock.Lock() 16 | defer lock.Unlock() 17 | 18 | if value, ok := randSources[base]; ok { 19 | return value 20 | } 21 | 22 | value := make([]int, base, base) 23 | for i := 0; i < base; i++ { 24 | value[i] = i 25 | } 26 | 27 | rand.Shuffle(base, func(i, j int) { 28 | value[i], value[j] = value[j], value[i] 29 | }) 30 | randSources[base] = value 31 | return value 32 | } 33 | 34 | // RateBarrier rand barrier 35 | type RateBarrier struct { 36 | source []int 37 | op uint64 38 | rate int 39 | base int 40 | } 41 | 42 | // NewRateBarrier returns a barrier based by 100 43 | func NewRateBarrier(rate int) *RateBarrier { 44 | return NewRateBarrierBase(rate, 100) 45 | } 46 | 47 | // NewRateBarrierBase returns a barrier with base 48 | func NewRateBarrierBase(rate, base int) *RateBarrier { 49 | return &RateBarrier{ 50 | source: createRandSourceByBase(base), 51 | rate: rate, 52 | base: base, 53 | } 54 | } 55 | 56 | // Allow returns true if allowed 57 | func (b *RateBarrier) Allow() bool { 58 | return b.source[int(atomic.AddUint64(&b.op, 1))%b.base] < b.rate 59 | } 60 | -------------------------------------------------------------------------------- /pkg/util/ip_util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | // ClientIP returns the real client IP 10 | func ClientIP(ctx *fasthttp.RequestCtx) string { 11 | clientIP := string(ctx.Request.Header.Peek("X-Forwarded-For")) 12 | if index := strings.IndexByte(clientIP, ','); index >= 0 { 13 | clientIP = clientIP[0:index] 14 | } 15 | clientIP = strings.TrimSpace(clientIP) 16 | if len(clientIP) > 0 { 17 | return clientIP 18 | } 19 | clientIP = strings.TrimSpace(string(ctx.Request.Header.Peek("X-Real-Ip"))) 20 | if len(clientIP) > 0 { 21 | return clientIP 22 | } 23 | return ctx.RemoteIP().String() 24 | } 25 | -------------------------------------------------------------------------------- /pkg/util/lru.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | 7 | "github.com/fagongzi/goetty" 8 | ) 9 | 10 | // Cache is an LRU cache. It is not safe for concurrent access. 11 | type Cache struct { 12 | sync.RWMutex 13 | 14 | // MaxBytes is the maximum bytes of cache entries before 15 | // an item is evicted. Zero means no limit. 16 | MaxBytes uint64 17 | current uint64 18 | 19 | // OnEvicted optionally specificies a callback function to be 20 | // executed when an entry is purged from the cache. 21 | OnEvicted func(key Key, value *goetty.ByteBuf) 22 | 23 | ll *list.List 24 | cache map[interface{}]*list.Element 25 | } 26 | 27 | // A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators 28 | type Key interface{} 29 | 30 | type entry struct { 31 | key Key 32 | value *goetty.ByteBuf 33 | } 34 | 35 | // NewLRUCache creates a new Cache. 36 | // If maxBytes is zero, the cache has no limit and it's assumed 37 | // that eviction is done by the caller. 38 | func NewLRUCache(maxBytes uint64, evictedFunc func(key Key, value *goetty.ByteBuf)) *Cache { 39 | return &Cache{ 40 | MaxBytes: maxBytes, 41 | ll: list.New(), 42 | cache: make(map[interface{}]*list.Element), 43 | OnEvicted: evictedFunc, 44 | } 45 | } 46 | 47 | // Add adds a value to the cache. 48 | func (c *Cache) Add(key Key, value *goetty.ByteBuf) { 49 | c.Lock() 50 | 51 | if c.cache == nil { 52 | c.cache = make(map[interface{}]*list.Element) 53 | c.ll = list.New() 54 | } 55 | if ee, ok := c.cache[key]; ok { 56 | c.ll.MoveToFront(ee) 57 | 58 | entry := ee.Value.(*entry) 59 | c.current -= uint64(value.Readable()) 60 | c.current += uint64(value.Readable()) 61 | entry.value = value 62 | c.Unlock() 63 | return 64 | } 65 | 66 | c.current += uint64(value.Readable()) 67 | ele := c.ll.PushFront(&entry{key, value}) 68 | c.cache[key] = ele 69 | if c.MaxBytes != 0 && c.current > c.MaxBytes { 70 | c.removeOldest() 71 | } 72 | c.Unlock() 73 | } 74 | 75 | // Get looks up a key's value from the cache. 76 | func (c *Cache) Get(key Key) (value *goetty.ByteBuf, ok bool) { 77 | c.RLock() 78 | 79 | if c.cache == nil { 80 | c.RUnlock() 81 | return 82 | } 83 | 84 | if ele, hit := c.cache[key]; hit { 85 | c.ll.MoveToFront(ele) 86 | c.RUnlock() 87 | return ele.Value.(*entry).value, true 88 | } 89 | 90 | c.RUnlock() 91 | return 92 | } 93 | 94 | // Remove removes the provided key from the cache. 95 | func (c *Cache) Remove(key Key) { 96 | c.Lock() 97 | 98 | if c.cache == nil { 99 | c.Unlock() 100 | return 101 | } 102 | if ele, hit := c.cache[key]; hit { 103 | c.removeElement(ele) 104 | } 105 | 106 | c.Unlock() 107 | } 108 | 109 | func (c *Cache) removeOldest() { 110 | if c.cache == nil { 111 | return 112 | } 113 | ele := c.ll.Back() 114 | if ele != nil { 115 | c.removeElement(ele) 116 | } 117 | } 118 | 119 | func (c *Cache) removeElement(e *list.Element) { 120 | c.ll.Remove(e) 121 | kv := e.Value.(*entry) 122 | delete(c.cache, kv.key) 123 | c.current -= uint64(kv.value.Readable()) 124 | if c.OnEvicted != nil { 125 | c.OnEvicted(kv.key, kv.value) 126 | } 127 | } 128 | 129 | // Len returns the number of items in the cache. 130 | func (c *Cache) Len() int { 131 | c.RLock() 132 | if c.cache == nil { 133 | c.RUnlock() 134 | return 0 135 | } 136 | value := c.ll.Len() 137 | c.RUnlock() 138 | return value 139 | } 140 | 141 | // Clear purges all stored items from the cache. 142 | func (c *Cache) Clear() { 143 | c.Lock() 144 | if c.OnEvicted != nil { 145 | for _, e := range c.cache { 146 | kv := e.Value.(*entry) 147 | c.OnEvicted(kv.key, kv.value) 148 | } 149 | } 150 | c.ll = nil 151 | c.cache = nil 152 | c.current = 0 153 | c.Unlock() 154 | } 155 | -------------------------------------------------------------------------------- /pkg/util/metric_push.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "strings" 12 | "time" 13 | 14 | "github.com/fagongzi/log" 15 | "github.com/fagongzi/util/task" 16 | "github.com/prometheus/client_golang/prometheus" 17 | "github.com/prometheus/common/expfmt" 18 | "github.com/prometheus/common/model" 19 | ) 20 | 21 | const contentTypeHeader = "Content-Type" 22 | 23 | var ( 24 | client = &http.Client{} 25 | defaultTimeout = time.Second * 15 26 | ) 27 | 28 | // MetricCfg is the metric configuration. 29 | type MetricCfg struct { 30 | Job string 31 | Instance string 32 | Address string 33 | DurationSync time.Duration 34 | } 35 | 36 | // NewMetricCfg returns metric cfg 37 | func NewMetricCfg(job, instance, address string, durationSync time.Duration) *MetricCfg { 38 | return &MetricCfg{ 39 | Job: job, 40 | Instance: instance, 41 | Address: address, 42 | DurationSync: durationSync, 43 | } 44 | } 45 | 46 | // StartMetricsPush start a push client 47 | func StartMetricsPush(runner *task.Runner, cfg *MetricCfg) { 48 | if nil == cfg || cfg.DurationSync == 0 || len(cfg.Address) == 0 { 49 | log.Info("metric: disable prometheus push client") 50 | return 51 | } 52 | 53 | *client = *http.DefaultClient 54 | client.Timeout = defaultTimeout 55 | 56 | log.Info("metric: start prometheus push client") 57 | runner.RunCancelableTask(func(ctx context.Context) { 58 | t := time.NewTicker(cfg.DurationSync) 59 | defer t.Stop() 60 | 61 | for { 62 | select { 63 | case <-ctx.Done(): 64 | log.Info("stop: prometheus push client stopped") 65 | t.Stop() 66 | return 67 | case <-t.C: 68 | err := doPush(cfg.Job, instanceGroupingKey(cfg.Instance), cfg.Address, prometheus.DefaultGatherer, "PUT") 69 | if err != nil { 70 | log.Errorf("metric: could not push metrics to prometheus pushgateway: errors:\n%+v", err) 71 | } 72 | } 73 | } 74 | }) 75 | } 76 | 77 | // instanceGroupingKey returns a label map with the only entry 78 | // {instance=""}. If instance is empty, use hostname instead. 79 | func instanceGroupingKey(instance string) map[string]string { 80 | if instance == "" { 81 | var err error 82 | if instance, err = os.Hostname(); err != nil { 83 | instance = "unknown" 84 | } 85 | } 86 | return map[string]string{"instance": instance} 87 | } 88 | 89 | func doPush(job string, grouping map[string]string, pushURL string, g prometheus.Gatherer, method string) error { 90 | if !strings.Contains(pushURL, "://") { 91 | pushURL = "http://" + pushURL 92 | } 93 | if strings.HasSuffix(pushURL, "/") { 94 | pushURL = pushURL[:len(pushURL)-1] 95 | } 96 | 97 | if strings.Contains(job, "/") { 98 | return fmt.Errorf("job contains '/': %s", job) 99 | } 100 | urlComponents := []string{url.QueryEscape(job)} 101 | for ln, lv := range grouping { 102 | if !model.LabelName(ln).IsValid() { 103 | return fmt.Errorf("grouping label has invalid name: %s", ln) 104 | } 105 | if strings.Contains(lv, "/") { 106 | return fmt.Errorf("value of grouping label %s contains '/': %s", ln, lv) 107 | } 108 | urlComponents = append(urlComponents, ln, lv) 109 | } 110 | pushURL = fmt.Sprintf("%s/metrics/job/%s", pushURL, strings.Join(urlComponents, "/")) 111 | 112 | mfs, err := g.Gather() 113 | if err != nil { 114 | return err 115 | } 116 | buf := &bytes.Buffer{} 117 | enc := expfmt.NewEncoder(buf, expfmt.FmtProtoDelim) 118 | // Check for pre-existing grouping labels: 119 | for _, mf := range mfs { 120 | for _, m := range mf.GetMetric() { 121 | for _, l := range m.GetLabel() { 122 | if l.GetName() == "job" { 123 | return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m) 124 | } 125 | if _, ok := grouping[l.GetName()]; ok { 126 | return fmt.Errorf( 127 | "pushed metric %s (%s) already contains grouping label %s", 128 | mf.GetName(), m, l.GetName(), 129 | ) 130 | } 131 | } 132 | } 133 | enc.Encode(mf) 134 | } 135 | req, err := http.NewRequest(method, pushURL, buf) 136 | if err != nil { 137 | return err 138 | } 139 | req.Header.Set(contentTypeHeader, string(expfmt.FmtProtoDelim)) 140 | resp, err := client.Do(req) 141 | if err != nil { 142 | return err 143 | } 144 | defer resp.Body.Close() 145 | if resp.StatusCode != 202 { 146 | body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. 147 | return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, pushURL, body) 148 | } 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /pkg/util/time.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // NowWithMillisecond returns timestamp with millisecond 8 | func NowWithMillisecond() int64 { 9 | return time.Now().UnixNano() / int64(time.Millisecond) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/util/version.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // set on build time 8 | var ( 9 | GitCommit = "" 10 | BuildTime = "" 11 | GoVersion = "" 12 | Version = "" 13 | ) 14 | 15 | // PrintVersion Print out version information 16 | func PrintVersion() { 17 | fmt.Println("Version : ", Version) 18 | fmt.Println("GitCommit: ", GitCommit) 19 | fmt.Println("BuildTime: ", BuildTime) 20 | fmt.Println("GoVersion: ", GoVersion) 21 | } 22 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | scrape_timeout: 10s 4 | evaluation_interval: 15s 5 | alerting: 6 | alertmanagers: 7 | - static_configs: 8 | - targets: [] 9 | scheme: http 10 | timeout: 10s 11 | scrape_configs: 12 | - job_name: prometheus 13 | honor_timestamps: true 14 | scrape_interval: 15s 15 | scrape_timeout: 10s 16 | metrics_path: /metrics 17 | scheme: http 18 | static_configs: 19 | - targets: 20 | - localhost:9090 21 | - pushgateway:9091 --------------------------------------------------------------------------------