├── .circleci └── config.yml ├── .github └── workflows │ └── go.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README-zh_CN.md ├── README.md ├── bf ├── bitset │ ├── bitset.go │ └── bitset_test.go ├── bloomfilter.go ├── bloomfilter │ ├── bloomfilter.go │ └── bloomfilter_test.go ├── hasher.go ├── hasher_test.go ├── rotate │ ├── compressor.go │ ├── rotate.go │ └── rotate_test.go ├── rpc │ ├── client │ │ ├── client.go │ │ └── main.go │ ├── rpc.go │ ├── rpc_test.go │ └── server │ │ └── server.go └── testutils │ └── testutils.go ├── cmd ├── check.go ├── cmd.go ├── graph.go ├── root.go └── run.go ├── config ├── config.go ├── config_test.go ├── parser.go └── uri.go ├── core ├── melody │ ├── backend_factory.go │ ├── encoding.go │ ├── executor.go │ ├── handler_factory.go │ ├── plugin.go │ ├── proxy_factory.go │ ├── router_engine.go │ └── sd.go └── version.go ├── coverage.sh ├── docs ├── DNS SRV.md ├── DNS.md ├── Docker常用启动脚本.md ├── ETCD使用介绍.md ├── HTTP公钥固定.md ├── JWT.md ├── MIME嗅探防御.md ├── Melody Server.xmind ├── Middleware命名空间.md ├── TLS和SSL协议.md ├── VuePress入门使用.md ├── img │ ├── example.png │ ├── melody.png │ ├── min-melody-logo.png │ ├── p-1.png │ ├── p-2.png │ ├── p-3.png │ └── struct.png ├── questions.md ├── 一致性算法.md ├── 令牌桶算法.md ├── 关于HTTP严格传输安全.md ├── 关键的结构体说明.md ├── 布隆过滤器.md ├── 点劫持保护.md └── 相关博客链接.md ├── encoding ├── encoding.go ├── json.go ├── register.go └── string.go ├── go.mod ├── go.sum ├── logging ├── log.go └── log_test.go ├── main.go ├── main_test.go ├── melody-cases ├── Makefile ├── coupon │ └── main.go ├── finance │ └── main.go ├── melody.json ├── profile │ └── main.go └── role │ └── main.go ├── melody.json ├── middleware ├── melody-alert │ ├── checker.go │ ├── config.go │ └── model │ │ └── warning.go ├── melody-bloomfilter │ └── bloomfilter.go ├── melody-botmonitor │ ├── gin │ │ ├── monitor.go │ │ └── monitor_test.go │ ├── melody │ │ └── config.go │ ├── monitor.go │ ├── monitor_bechmark_test.go │ └── monitor_test.go ├── melody-circuitbreaker │ ├── gobreaker.go │ └── proxy │ │ ├── proxy.go │ │ └── proxy_test.go ├── melody-consul │ ├── client.go │ └── config.go ├── melody-cors │ ├── cors.go │ ├── cors_test.go │ └── gin │ │ ├── cors.go │ │ └── cors_test.go ├── melody-etcd │ ├── client.go │ ├── config.go │ ├── subscriber.go │ └── subscriber_test.go ├── melody-gelf │ └── log.go ├── melody-gologging │ └── log.go ├── melody-httpsecure │ ├── gin │ │ ├── httpsecure.go │ │ └── httpsecure_test.go │ ├── httpsecure.go │ └── httpsecure_test.go ├── melody-influxdb │ ├── buffer.go │ ├── config.go │ ├── counter │ │ └── counter.go │ ├── gauge │ │ └── gauge.go │ ├── handler.go │ ├── histogram │ │ └── histogram.go │ ├── influxdb.go │ ├── middleware │ │ └── cors.go │ ├── refresh │ │ └── refresh.go │ ├── response │ │ └── response.go │ └── ws │ │ ├── alert_handler.go │ │ ├── convert │ │ └── convert.go │ │ ├── debug_handler.go │ │ ├── handler │ │ └── chart.go │ │ ├── query.go │ │ ├── requests_handler.go │ │ ├── router_handler.go │ │ ├── runtime_handler.go │ │ ├── struct.go │ │ ├── template.go │ │ ├── timer.go │ │ └── websocket.go ├── melody-jose │ ├── fixtures │ │ ├── jwks.json │ │ ├── private.json │ │ ├── public.json │ │ └── symmetric.json │ ├── gin │ │ ├── jose.go │ │ ├── jose_example_test.go │ │ └── jose_test.go │ ├── jose.go │ ├── jose_test.go │ ├── jwk.go │ ├── jwk_example_test.go │ ├── jwk_test.go │ ├── jws.go │ ├── jws_test.go │ ├── rejecter.go │ └── rejecter_test.go ├── melody-jsonschema │ ├── jsonschema.go │ └── jsonschema_test.go ├── melody-logstash │ └── logger.go ├── melody-martian │ ├── context.go │ ├── martian.go │ ├── martian_test.go │ ├── register.go │ ├── register │ │ └── register.go │ └── static.go ├── melody-metrics │ ├── gin │ │ └── metrics.go │ ├── metrics.go │ ├── proxy.go │ ├── router.go │ └── stats.go ├── melody-opencensus │ └── opencensus.go ├── melody-ratelimit │ ├── juju │ │ ├── juju.go │ │ ├── proxy │ │ │ ├── proxy.go │ │ │ ├── proxy_benchmark_test.go │ │ │ └── proxy_test.go │ │ └── router │ │ │ ├── gin │ │ │ └── gin.go │ │ │ ├── router.go │ │ │ └── router_test.go │ ├── melodyrate.go │ ├── melodyrate_test.go │ └── pseudoFNV64a.go ├── melody-rss │ └── rss.go ├── melody-viper │ └── viper.go └── melody-xml │ └── xml.go ├── plugin └── plugin.go ├── proxy ├── balancing.go ├── concurrent.go ├── concurrent_test.go ├── factory.go ├── factory_test.go ├── formatter.go ├── formatter_test.go ├── http.go ├── http_response.go ├── http_test.go ├── merging.go ├── merging_test.go ├── proxy.go ├── proxy_test.go ├── register.go ├── register_test.go ├── request.go ├── shadow.go ├── shadow_test.go ├── static.go └── static_test.go ├── register ├── internal │ └── untyped.go ├── register.go └── register_test.go ├── router ├── gin │ ├── debug.go │ ├── endpoint.go │ ├── render.go │ ├── render_test.go │ ├── router.go │ └── router_test.go └── router.go ├── sd ├── dnssrv │ ├── subscriber.go │ └── subscriber_test.go ├── loadbalancing.go ├── register.go └── subscriber.go └── transport └── http ├── client ├── executor.go └── status.go └── server ├── plugin ├── plugin.go └── server.go └── server.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version 9 | - image: circleci/golang:1.14 10 | 11 | # Specify service dependencies here if necessary 12 | # CircleCI maintains a library of pre-built images 13 | # documented at https://circleci.com/docs/2.0/circleci-images/ 14 | # - image: circleci/postgres:9.4 15 | 16 | #### TEMPLATE_NOTE: go expects specific checkout path representing url 17 | #### expecting it in the form of 18 | #### /go/src/github.com/circleci/go-tool 19 | #### /go/src/bitbucket.org/circleci/go-tool 20 | working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}} 21 | steps: 22 | - checkout 23 | 24 | # specify any bash command here prefixed with `run: ` 25 | - run: go get -v -t -d ./... 26 | - run: make test 27 | - run: ls -la 28 | - run: pwd -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Melody code build 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | paths-ignore: 7 | - './docs/**' 8 | - '**.md' 9 | pull_request: 10 | branches: 11 | - '*' 12 | paths-ignore: 13 | - './docs/**' 14 | - '**.md' 15 | jobs: 16 | build: 17 | name: Build 18 | runs-on: ubuntu-latest 19 | steps: 20 | 21 | - name: Set up Go 1.13 22 | uses: actions/setup-go@v1 23 | with: 24 | go-version: 1.13 25 | id: go 26 | 27 | - name: Check out code into the Go module directory 28 | uses: actions/checkout@v2 29 | 30 | - name: Get dependencies 31 | run: | 32 | 33 | go get -v -t -d ./... 34 | if [ -f Gopkg.toml ]; then 35 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 36 | dep ensure 37 | fi 38 | 39 | - name: Test and Build 40 | run: make all 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 4 | 5 | *.iml 6 | 7 | ## Directory-based project format: 8 | .vscode 9 | .idea/ 10 | # if you remove the above rule, at least ignore the following: 11 | 12 | # User-specific stuff: 13 | # .idea/workspace.xml 14 | # .idea/tasks.xml 15 | # .idea/dictionaries 16 | 17 | # Sensitive or high-churn files: 18 | # .idea/dataSources.ids 19 | # .idea/dataSources.xml 20 | # .idea/sqlDataSources.xml 21 | # .idea/dynamic.xml 22 | # .idea/uiDesigner.xml 23 | 24 | # Gradle: 25 | # .idea/gradle.xml 26 | # .idea/libraries 27 | *.log 28 | 29 | # Mongo Explorer plugin: 30 | # .idea/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.ipr 34 | *.iws 35 | 36 | ## Plugin-specific files: 37 | 38 | # IntelliJ 39 | /out/ 40 | 41 | # mpeltonen/sbt-idea plugin 42 | .idea_modules/ 43 | 44 | # JIRA plugin 45 | atlassian-ide-plugin.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | ### Maven template 52 | target/ 53 | pom.xml.tag 54 | pom.xml.releaseBackup 55 | pom.xml.versionsBackup 56 | pom.xml.next 57 | release.properties 58 | dependency-reduced-pom.xml 59 | buildNumber.properties 60 | .mvn/timing.properties 61 | melody.exe 62 | *.out 63 | *.cover 64 | .cover/ 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13.x" 5 | - "1.12.x" 6 | - "1.11.x" 7 | 8 | before_install: 9 | - mkdir -p /home/travis/gopath-tmp 10 | - export GOPATH="/home/travis/gopath-tmp" 11 | 12 | script: 13 | - make coveralls -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all deps test build benchmark coveralls 2 | 3 | DEP_VERSION=0.5.0 4 | OS := $(shell uname | tr '[:upper:]' '[:lower:]') 5 | GIT_COMMIT := $(shell git rev-parse --short=7 HEAD) 6 | 7 | 8 | all: test build 9 | 10 | test: 11 | go generate ./... 12 | go test -cover -race ./... 13 | 14 | benchmark: 15 | @mkdir -p bench_res 16 | @touch bench_res/${GIT_COMMIT}.out 17 | @go test -run none -bench . -benchmem ./... >> bench_res/${GIT_COMMIT}.out 18 | 19 | build: 20 | @echo "Build ..." 21 | @go build 22 | @echo "You can use melody now!" 23 | 24 | coveralls: all 25 | go get github.com/mattn/goveralls 26 | go install github.com/mattn/goveralls 27 | sh coverage.sh --coveralls 28 | -------------------------------------------------------------------------------- /bf/bitset/bitset.go: -------------------------------------------------------------------------------- 1 | package bitset 2 | 3 | import ( 4 | "github.com/tmthrgd/go-bitset" 5 | "melody/bf" 6 | ) 7 | 8 | type BitSet struct { 9 | bs bitset.Bitset 10 | hasher bf.Hash 11 | } 12 | 13 | // NewBitSet constructor for BitSet with an array of m bits 14 | func NewBitSet(m uint) *BitSet { 15 | return &BitSet{bitset.New(m), bf.MD5} 16 | } 17 | 18 | // Add element to bitset 19 | func (bs *BitSet) Add(elem []byte) { 20 | bs.bs.Set(bs.hasher(elem)[0] % bs.bs.Len()) 21 | } 22 | 23 | // Check element in bitset 24 | func (bs *BitSet) Check(elem []byte) bool { 25 | return bs.bs.IsSet(bs.hasher(elem)[0] % bs.bs.Len()) 26 | } 27 | 28 | // Union 组合两个 bitSet 29 | func (bs *BitSet) Union(that interface{}) (float64, error) { 30 | other, ok := that.(*BitSet) 31 | if !ok { 32 | return bs.getCount(), bf.ErrImpossibleToTreat 33 | } 34 | 35 | bs.bs.Union(bs.bs, other.bs) 36 | return bs.getCount(), nil 37 | } 38 | 39 | func (bs *BitSet) getCount() float64 { 40 | return float64(bs.bs.Count()) / float64(bs.bs.Len()) 41 | } 42 | -------------------------------------------------------------------------------- /bf/bitset/bitset_test.go: -------------------------------------------------------------------------------- 1 | package bitset 2 | 3 | import ( 4 | "melody/bf" 5 | "melody/bf/testutils" 6 | "testing" 7 | ) 8 | 9 | func TestBitSet(t *testing.T) { 10 | testutils.CallSet(t, NewBitSet(24)) 11 | } 12 | 13 | func TestBitSet_Union_ok(t *testing.T) { 14 | set1 := NewBitSet(24) 15 | set2 := NewBitSet(24) 16 | 17 | testutils.CallSetUnion(t, set1, set2) 18 | } 19 | 20 | func TestBitSet_Union_ko(t *testing.T) { 21 | set1 := NewBitSet(24) 22 | set2 := 24 23 | 24 | if _, err := set1.Union(set2); err != bf.ErrImpossibleToTreat { 25 | t.Errorf("Unexpected error, %v", err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bf/bloomfilter.go: -------------------------------------------------------------------------------- 1 | // Package bloomfilter contains common data and interfaces needed to implement bloomfilters. 2 | // 3 | // 理论基础: http://llimllib.github.io/bloomfilter-tutorial/zh_CN/ 4 | // 我们实现了三种 bloomfilter :衍生自bitSet的、 sliding bloomfilters、 rpc bloomfilter 5 | package bf 6 | 7 | import "math" 8 | 9 | type BloomFilter interface { 10 | Add([]byte) 11 | Check([]byte) bool 12 | Union(interface{}) (float64, error) 13 | } 14 | 15 | // P - 失误几率 16 | // N - 存到过滤器的element的个数 17 | // HashName - "default" or "optimal" 18 | type Config struct { 19 | N uint 20 | P float64 21 | HashName string 22 | } 23 | 24 | // 给 sliding bf 的 previous 用的 25 | var EmptyConfig = Config{ 26 | N: 2, 27 | P: .5, 28 | } 29 | 30 | // M bit array 的长度 31 | func M(n uint, p float64) uint { 32 | return uint(math.Ceil(-(float64(n) * math.Log(p)) / math.Log(math.Pow(2.0, math.Log(2.0))))) 33 | } 34 | 35 | // K hash函数的个数 36 | func K(m, n uint) uint { 37 | return uint(math.Ceil(math.Log(2.0) * float64(m) / float64(n))) 38 | } 39 | 40 | type EmptySet int 41 | 42 | // Check implementation for EmptySet 43 | func (e EmptySet) Check(_ []byte) bool { return false } 44 | 45 | // Add implementation for EmptySet 46 | func (e EmptySet) Add(_ []byte) {} 47 | 48 | // Union implementation for EmptySet 49 | func (e EmptySet) Union(interface{}) (float64, error) { return -1, nil } 50 | -------------------------------------------------------------------------------- /bf/bloomfilter/bloomfilter_test.go: -------------------------------------------------------------------------------- 1 | package bloomfilter 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "melody/bf" 7 | "melody/bf/testutils" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestBloomFilter(t *testing.T) { 13 | testutils.CallSet(t, New(testutils.TestCfg)) 14 | } 15 | 16 | func TestBloomFilter_Union_ok(t *testing.T) { 17 | set1 := New(testutils.TestCfg) 18 | set2 := New(testutils.TestCfg) 19 | 20 | testutils.CallSetUnion(t, set1, set2) 21 | } 22 | 23 | func TestBloomFilter_Union_koIncorrectType(t *testing.T) { 24 | set1 := New(testutils.TestCfg) 25 | set2 := 24 26 | 27 | if _, err := set1.Union(set2); err != bf.ErrImpossibleToTreat { 28 | t.Errorf("Unexpected error, %v", err) 29 | } 30 | } 31 | 32 | func TestBloomFilter_Union_koDifferentM(t *testing.T) { 33 | set1 := New(testutils.TestCfg) 34 | set2 := New(testutils.TestCfg) 35 | set2.m = 111 36 | if _, err := set1.Union(set2); err == nil || !strings.Contains(err.Error(), "!= m2(111)") { 37 | t.Errorf("Unexpected error, %v", err) 38 | } 39 | } 40 | 41 | func TestBloomFilter_Union_koDifferentK(t *testing.T) { 42 | set1 := New(testutils.TestCfg) 43 | set2 := New(testutils.TestCfg) 44 | set2.k = 111 45 | if _, err := set1.Union(set2); err == nil || !strings.Contains(err.Error(), "!= k2(111)") { 46 | t.Errorf("Unexpected error, %v", err) 47 | } 48 | } 49 | 50 | func TestBloomFilter_gobEncoder(t *testing.T) { 51 | bf1 := New(testutils.TestCfg) 52 | bf1.Add([]byte("casa")) 53 | bf1.Add([]byte("grrrrr")) 54 | bf1.Add([]byte("something")) 55 | 56 | serialized := new(bytes.Buffer) 57 | if err := gob.NewEncoder(serialized).Encode(bf1); err != nil { 58 | t.Errorf("error encoding BF, %s", err.Error()) 59 | } 60 | 61 | bf2 := new(BloomFilter) 62 | if err := gob.NewDecoder(serialized).Decode(bf2); err != nil { 63 | t.Errorf("error encoding BF, %s", err.Error()) 64 | } 65 | 66 | if !bf2.Check([]byte("casa")) { 67 | t.Error("error: \"casa\" not found") 68 | } 69 | if !bf2.Check([]byte("grrrrr")) { 70 | t.Error("error: \"grrrrr\" not found") 71 | } 72 | if !bf2.Check([]byte("something")) { 73 | t.Error("error: \"something\" not found") 74 | } 75 | } 76 | 77 | func TestUnmarshalBinary_ko(t *testing.T) { 78 | set1 := New(testutils.TestCfg) 79 | if err := set1.UnmarshalBinary([]byte{}); err == nil { 80 | t.Error("should have given error") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /bf/hasher.go: -------------------------------------------------------------------------------- 1 | package bf 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "encoding/binary" 7 | "fmt" 8 | "hash" 9 | "hash/crc64" 10 | "hash/fnv" 11 | ) 12 | 13 | type Hash func([]byte) []uint 14 | type HashFactory func(uint) []Hash 15 | 16 | const ( 17 | HASHER_DEFAULT = "default" 18 | HASHER_OPTIMAL = "optimal" 19 | ) 20 | 21 | var ( 22 | defaultHashers = []Hash{ 23 | MD5, 24 | CRC64, 25 | SHA1, 26 | FNV64, 27 | FNV128, 28 | } 29 | 30 | HashFactoryNames = map[string]HashFactory{ 31 | HASHER_DEFAULT: DefaultHashFactory, 32 | HASHER_OPTIMAL: OptimalHashFactory, 33 | } 34 | 35 | ErrImpossibleToTreat = fmt.Errorf("unable to union") 36 | 37 | MD5 = HashWrapper(md5.New()) 38 | SHA1 = HashWrapper(sha1.New()) 39 | CRC64 = HashWrapper(crc64.New(crc64.MakeTable(crc64.ECMA))) 40 | FNV64 = HashWrapper(fnv.New64()) 41 | FNV128 = HashWrapper(fnv.New128()) 42 | ) 43 | 44 | // 取 K 个hash函数 45 | func DefaultHashFactory(k uint) []Hash { 46 | if k > uint(len(defaultHashers)) { 47 | k = uint(len(defaultHashers)) 48 | } 49 | return defaultHashers[:k] 50 | } 51 | 52 | // 最优hash工厂 53 | // FNV能快速hash大量数据并保持较小的冲突率,它的高度分散使它适用于hash一些非常相近的字符串, 54 | // 比如URL,hostname,文件名,text,IP地址等。 55 | func OptimalHashFactory(k uint) []Hash { 56 | return []Hash{ 57 | func(b []byte) []uint { 58 | hs := FNV128(b) 59 | out := make([]uint, k) 60 | 61 | for i := range out { 62 | // 128位 => len(out) = 2 63 | out[i] = hs[0] + uint(i)*hs[1] 64 | } 65 | return out 66 | }, 67 | } 68 | } 69 | 70 | func HashWrapper(h hash.Hash) Hash { 71 | return func(elem []byte) []uint { 72 | h.Reset() 73 | h.Write(elem) 74 | result := h.Sum(nil) 75 | // 例如md5 128位 = 16字节 len(result) = 16 76 | out := make([]uint, len(result)/8) // len(out) = 2 77 | for i := 0; i < len(result)/8; i++ { 78 | // binary.LittleEndian.Uint64(result[i*8 : (i+1)*8]) 79 | // 取 result 中连续8个字节转换成 uint64 why??? 80 | // Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。 81 | out[i] = uint(binary.LittleEndian.Uint64(result[i*8 : (i+1)*8])) 82 | } 83 | return out 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /bf/hasher_test.go: -------------------------------------------------------------------------------- 1 | package bf 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestHasher(t *testing.T) { 10 | for _, hash := range defaultHashers { 11 | array1 := []byte{1, 2, 3} 12 | if !reflect.DeepEqual(hash(array1), hash(array1)) { 13 | t.Error("undeterministic") 14 | } 15 | } 16 | } 17 | 18 | func BenchmarkHasher(b *testing.B) { 19 | for k, hash := range defaultHashers { 20 | b.Run(fmt.Sprintf("hasher %d", k), func(b *testing.B) { 21 | for i := 0; i < b.N; i++ { 22 | array1 := []byte{1, 2, 3} 23 | hash(array1) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func TestDefaultHashFactory(t *testing.T) { 30 | for _, hash := range DefaultHashFactory(23) { 31 | array1 := []byte{1, 2, 3} 32 | if !reflect.DeepEqual(hash(array1), hash(array1)) { 33 | t.Error("undeterministic") 34 | } 35 | } 36 | } 37 | 38 | func TestOptimalHashFactory(t *testing.T) { 39 | for _, hash := range OptimalHashFactory(23) { 40 | array1 := []byte{1, 2, 3} 41 | if !reflect.DeepEqual(hash(array1), hash(array1)) { 42 | t.Error("undeterministic") 43 | } 44 | } 45 | } 46 | 47 | func BenchmarkOptimalHashFactory(b *testing.B) { 48 | for k, hash := range OptimalHashFactory(23) { 49 | b.Run(fmt.Sprintf("hasher %d", k), func(b *testing.B) { 50 | for i := 0; i < b.N; i++ { 51 | array1 := []byte{1, 2, 3} 52 | hash(array1) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bf/rotate/compressor.go: -------------------------------------------------------------------------------- 1 | package rotate 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | ) 7 | 8 | type Compressor interface { 9 | NewWriter(io.Writer) io.WriteCloser 10 | NewReader(io.Reader) (io.Reader, error) 11 | } 12 | 13 | var compressor Compressor = new(Gzip) 14 | 15 | func SetCompressor(c Compressor) { 16 | compressor = c 17 | } 18 | 19 | type Gzip int 20 | 21 | func (g *Gzip) NewWriter(w io.Writer) io.WriteCloser { 22 | return gzip.NewWriter(w) 23 | } 24 | 25 | func (g *Gzip) NewReader(r io.Reader) (io.Reader, error) { 26 | return gzip.NewReader(r) 27 | } 28 | -------------------------------------------------------------------------------- /bf/rpc/client/client.go: -------------------------------------------------------------------------------- 1 | // Package client implements an rpc client for the BloomFilter, along with Add and Check methods. 2 | package main 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "melody/bf/rotate" 8 | rpc_bf "melody/bf/rpc" 9 | "net/rpc" 10 | ) 11 | 12 | // BloomFilter rpc client type 13 | type BloomFilter struct { 14 | client *rpc.Client 15 | } 16 | 17 | // New creates a new bloomfilter rpc client with address 18 | func New(address string) (*BloomFilter, error) { 19 | client, err := rpc.Dial("tcp", address) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &BloomFilter{client}, nil 24 | } 25 | 26 | // Add element through bloomfilter rpc client 27 | func (b *BloomFilter) Add(elem []byte) { 28 | var addOutput rpc_bf.AddOutput 29 | if err := b.client.Call("BloomFilterRPC.Add", rpc_bf.AddInput{[][]byte{elem}}, &addOutput); err != nil { 30 | fmt.Println("error on adding bloomfilter:", err.Error()) 31 | } 32 | } 33 | 34 | // Check present element through bloomfilter rpc client 35 | func (b *BloomFilter) Check(elem []byte) bool { 36 | var checkOutput rpc_bf.CheckOutput 37 | if err := b.client.Call("BloomFilterRPC.Check", rpc_bf.CheckInput{[][]byte{elem}}, &checkOutput); err != nil { 38 | fmt.Println("error on check bloomfilter:", err.Error()) 39 | return false 40 | } 41 | for _, v := range checkOutput.Checks { 42 | if !v { 43 | return false 44 | } 45 | } 46 | return true 47 | } 48 | 49 | // Union element through bloomfilter rpc client with sliding bloomfilters 50 | func (b *BloomFilter) Union(that interface{}) (float64, error) { 51 | v, ok := that.(*rotate.BloomFilter) 52 | if !ok { 53 | return -1.0, errors.New("invalide argument to Union, expected rotate.BloomFilter") 54 | } 55 | var unionOutput rpc_bf.UnionOutput 56 | if err := b.client.Call("BloomFilterRPC.Union", rpc_bf.UnionInput{v}, &unionOutput); err != nil { 57 | fmt.Println("error on union bloomfilter:", err.Error()) 58 | return -1.0, err 59 | } 60 | 61 | return unionOutput.Capacity, nil 62 | } 63 | 64 | func (b *BloomFilter) Close() { 65 | b.client.Close() 66 | } 67 | -------------------------------------------------------------------------------- /bf/rpc/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | bloomFilter, err := New("127.0.0.1:9999") 7 | if err != nil { 8 | fmt.Println("err: " + err.Error()) 9 | return 10 | } 11 | defer bloomFilter.Close() 12 | 13 | bloomFilter.Add([]byte("BEARER eyJhbGciOiJSUzI1NiIsImtpZCI6Im1lbG9keSJ9.eyJOaWNrTmFtZSI6ImthcmwiLCJSb2xlSWQiOiIyIiwiVVVJRCI6IjZmMTU4NWQ1LTYzNmUtMTFlYS1hZDk3LTI4N2ZjZjEzZjZmNSIsImV4cCI6MTU4OTM2OTA4MSwiaWF0IjoxNTg5MzYxODgxLCJpc3MiOiJNZWxvZHkifQ.Sr_yYN_UN-E4KakGvhqw43YgCMH6uwjrPHZ02cuFNE-PhH-ujLSsKWgQdt9VSGZ_D1YiSqIvjWv5hC-zKsM0CLt-EQ0JyKbEkErSRKs_GmiV1S-8eSbNTKSlWeE8UzfdsQoJwUwFPJb8VzPrQTXEqokMPE6GoAeU1y73jBhPJvAQHmcw3_kbjsoDQ252_UnBujlXc5XEqdSxjYKg85nN5z-Rl28k39KMLf4yhpUfSt2qa2ENkz8RliqlucAPKvOWw03zLC-KRfWQ8ekw1R4rAHWNsL3p0UOYlZCJfNReYBhFuB470-GdP-f54tGNzhh6zPqbA7qBrObdIIPVkCaarA")) 14 | } 15 | -------------------------------------------------------------------------------- /bf/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc_bf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "melody/bf/rotate" 7 | ) 8 | 9 | var ( 10 | ErrNoBloomFilterInitialized = fmt.Errorf("BloomFilter not initialized") 11 | bf *rotate.BloomFilter 12 | ) 13 | 14 | type Config struct { 15 | rotate.Config 16 | Port int 17 | } 18 | 19 | type BloomFilterRPC int 20 | 21 | type BloomFilter struct { 22 | BloomFilterRPC 23 | } 24 | 25 | func (b *BloomFilter) Get() *rotate.BloomFilter { 26 | return bf 27 | } 28 | 29 | func (b *BloomFilter) Close() { 30 | if bf != nil { 31 | bf.Close() 32 | } 33 | } 34 | 35 | // New 初始化了 df 这个变量,并且返回一个新的 bf 36 | // 如果想使用 bf 请使用 Get 方法 37 | func New(ctx context.Context, cfg Config) *BloomFilter { 38 | if bf != nil { 39 | bf.Close() 40 | } 41 | 42 | bf = rotate.New(ctx, cfg.Config) 43 | 44 | return new(BloomFilter) 45 | } 46 | 47 | // Add 函数的 input 48 | // 使用二维数组可以批量 Add 49 | type AddInput struct { 50 | Elems [][]byte 51 | } 52 | 53 | // Add 函数的 output 54 | type AddOutput struct { 55 | Count int 56 | } 57 | 58 | func (b *BloomFilterRPC) Add(in AddInput, out *AddOutput) error { 59 | fmt.Println("add:", in.Elems) 60 | defer func() { fmt.Println("added elements:", out.Count) }() 61 | 62 | if bf == nil { 63 | out.Count = 0 64 | return ErrNoBloomFilterInitialized 65 | } 66 | 67 | k := 0 68 | for _, elem := range in.Elems { 69 | bf.Add(elem) 70 | k++ 71 | } 72 | out.Count = k 73 | 74 | return nil 75 | } 76 | 77 | // CheckInput 函数的 input 78 | type CheckInput struct { 79 | Elems [][]byte 80 | } 81 | 82 | // CheckOutput 函数的 output 83 | type CheckOutput struct { 84 | Checks []bool 85 | } 86 | 87 | func (b *BloomFilterRPC) Check(in CheckInput, out *CheckOutput) error { 88 | fmt.Println("check:", in.Elems) 89 | defer func() { fmt.Println("checked elements:", out.Checks) }() 90 | 91 | checkRes := make([]bool, len(in.Elems)) 92 | 93 | if bf == nil { 94 | out.Checks = checkRes 95 | return ErrNoBloomFilterInitialized 96 | } 97 | 98 | for i, elem := range in.Elems { 99 | checkRes[i] = bf.Check(elem) 100 | } 101 | out.Checks = checkRes 102 | 103 | return nil 104 | } 105 | 106 | // union 函数的 input 107 | type UnionInput struct { 108 | BF *rotate.BloomFilter 109 | } 110 | 111 | // union 函数的 output 112 | type UnionOutput struct { 113 | Capacity float64 114 | } 115 | 116 | func (b *BloomFilterRPC) Union(in UnionInput, out *UnionOutput) error { 117 | fmt.Println("union:", in.BF) 118 | defer func() { fmt.Println("union resulting capacity:", out.Capacity) }() 119 | 120 | if bf == nil { 121 | out.Capacity = 0 122 | return ErrNoBloomFilterInitialized 123 | } 124 | 125 | var err error 126 | out.Capacity, err = bf.Union(in.BF) 127 | 128 | return err 129 | } 130 | -------------------------------------------------------------------------------- /bf/rpc/server/server.go: -------------------------------------------------------------------------------- 1 | // Package server implements an rpc server for the bloomfilter, registering a bloomfilter and accepting a tcp listener. 2 | package server 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | rpc_bf "melody/bf/rpc" 8 | "net" 9 | "net/rpc" 10 | ) 11 | 12 | func New(ctx context.Context, cfg rpc_bf.Config) *rpc_bf.BloomFilter { 13 | bf := rpc_bf.New(ctx, cfg) 14 | 15 | go Serve(ctx, cfg.Port, bf) 16 | 17 | return bf 18 | } 19 | 20 | // Serve creates an rpc server, registers a bloomfilter, accepts a tcp listener and closes when catching context done 21 | func Serve(ctx context.Context, port int, bf *rpc_bf.BloomFilter) { 22 | s := rpc.NewServer() 23 | 24 | if err := s.Register(&bf.BloomFilterRPC); err != nil { 25 | fmt.Println("server register error:", err.Error()) 26 | return 27 | } 28 | 29 | l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 30 | if err != nil { 31 | fmt.Println("unable to setup RPC listener:", err.Error()) 32 | return 33 | } 34 | 35 | go func() { 36 | for { 37 | select { 38 | case <-ctx.Done(): 39 | l.Close() 40 | bf.Close() 41 | return 42 | } 43 | } 44 | }() 45 | 46 | s.Accept(l) 47 | } 48 | -------------------------------------------------------------------------------- /bf/testutils/testutils.go: -------------------------------------------------------------------------------- 1 | // Package testutils contains utils for the tests. 2 | package testutils 3 | 4 | import ( 5 | "melody/bf" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | TestCfg = bf.Config{ 11 | N: 100, 12 | P: 0.001, 13 | HashName: bf.HASHER_OPTIMAL, 14 | } 15 | 16 | TestCfg2 = bf.Config{ 17 | N: 100, 18 | P: 0.00001, 19 | HashName: bf.HASHER_OPTIMAL, 20 | } 21 | 22 | TestCfg3 = bf.Config{ 23 | N: 100, 24 | P: 0.001, 25 | HashName: bf.HASHER_DEFAULT, 26 | } 27 | ) 28 | 29 | func CallSet(t *testing.T, set bf.BloomFilter) { 30 | set.Add([]byte{1, 2, 3}) 31 | if !set.Check([]byte{1, 2, 3}) { 32 | t.Error("failed check") 33 | } 34 | 35 | if set.Check([]byte{1, 2, 4}) { 36 | t.Error("unexpected check") 37 | } 38 | } 39 | 40 | func CallSetUnion(t *testing.T, set1, set2 bf.BloomFilter) { 41 | elem := []byte{1, 2, 3} 42 | set1.Add(elem) 43 | if !set1.Check(elem) { 44 | t.Error("failed add set1 before union") 45 | return 46 | } 47 | 48 | if set2.Check(elem) { 49 | t.Error("unexpected check to union of set2") 50 | return 51 | } 52 | 53 | if _, err := set2.Union(set1); err != nil { 54 | t.Error("failed union set1 to set2", err.Error()) 55 | return 56 | } 57 | 58 | if !set2.Check(elem) { 59 | t.Error("failed union check of set1 to set2") 60 | return 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cmd/check.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func checkFunc(cmd *cobra.Command, args []string) { 10 | //检查配置文件是否为空 11 | if cfgFilePath == "" { 12 | cmd.Println("Please, provide the path to your config file") 13 | return 14 | } 15 | cmd.Printf("Parsing configuration file: %s\n", cfgFilePath) 16 | v, err := parser.Parse(cfgFilePath) 17 | 18 | //如果开启了debug 19 | if debug { 20 | cmd.Printf("Parsed configuration: CacheTTL: %s, Port: %d\n", v.CacheTTL.String(), v.Port) 21 | cmd.Printf("Hosts: %v\n", v.Host) 22 | 23 | cmd.Printf("Extra (%d):\n", len(v.ExtraConfig)) 24 | for k, e := range v.ExtraConfig { 25 | cmd.Printf(" %s: %v\n", k, e) 26 | } 27 | 28 | cmd.Printf("Endpoints (%d):\n", len(v.Endpoints)) 29 | for _, endpoint := range v.Endpoints { 30 | cmd.Printf("\tEndpoint: %s, Method: %s, CacheTTL: %s, Concurrent: %d, QueryString: %v\n", 31 | endpoint.Endpoint, endpoint.Method, endpoint.CacheTTL.String(), 32 | endpoint.ConcurrentCalls, endpoint.QueryString) 33 | 34 | cmd.Printf("\tExtra (%d):\n", len(endpoint.ExtraConfig)) 35 | for k, e := range endpoint.ExtraConfig { 36 | cmd.Printf("\t %s: %v\n", k, e) 37 | } 38 | 39 | cmd.Printf("\tBackends (%d):\n", len(endpoint.Backends)) 40 | for _, backend := range endpoint.Backends { 41 | cmd.Printf("\t\tURL: %s, Method: %s\n", backend.URLPattern, backend.Method) 42 | cmd.Printf("\t\t\tTimeout: %s, Target: %s, Mapping: %v, BL: %v, WL: %v, Group: %v\n", 43 | backend.Timeout, backend.Target, backend.Mapping, backend.Blacklist, backend.Whitelist, 44 | backend.Group) 45 | cmd.Printf("\t\t\tHosts: %v\n", backend.Host) 46 | cmd.Printf("\t\t\tExtra (%d):\n", len(backend.ExtraConfig)) 47 | for k, e := range backend.ExtraConfig { 48 | cmd.Printf("\t\t\t %s: %v\n", k, e) 49 | } 50 | } 51 | } 52 | 53 | } 54 | //如果错误 55 | if err != nil { 56 | cmd.Println("ERROR parsing the configuration file.\n", err.Error()) 57 | os.Exit(1) 58 | return 59 | } 60 | 61 | cmd.Println("Syntax OK!") 62 | } 63 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "melody/config" 6 | "os" 7 | ) 8 | 9 | //Executor defines the func that contains some prepration handles of start server. 10 | type Executor func(config.ServiceConfig) 11 | 12 | //Execute for other method to call. 13 | func Execute(configParser config.Parser, executor Executor) { 14 | parser = configParser 15 | run = executor 16 | if err := rootCmd.Execute(); err != nil { 17 | fmt.Println(err) 18 | os.Exit(1) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cmd/graph.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/spf13/cobra" 6 | "io" 7 | "melody/config" 8 | "os" 9 | "text/template" 10 | ) 11 | 12 | func graphFunc(cmd *cobra.Command, args []string) { 13 | if cfgFilePath == "" { 14 | cmd.Println("Please provide the path to your melody config file ") 15 | return 16 | } 17 | serviceConfig, err := parser.Parse(cfgFilePath) 18 | if err != nil { 19 | cmd.Printf("ERROR parsing the melody config file: %s\n", err.Error()) 20 | os.Exit(-1) 21 | } 22 | writeToDot(os.Stdout, serviceConfig, cmd) 23 | } 24 | 25 | func writeToDot(writer io.Writer, config config.ServiceConfig, cmd *cobra.Command) { 26 | t := template.New("dot") 27 | var buf bytes.Buffer 28 | if err := template.Must(t.Parse(tmplGraph)).Execute(&buf, config); err != nil { 29 | cmd.Println("ERROR convert data to dot error:", err) 30 | } 31 | buf.WriteTo(writer) 32 | } 33 | 34 | const tmplGraph = `digraph melody { {{ $port := .Port }} 35 | label="Melody Gateway"; 36 | labeljust="l"; 37 | fontname="Ubuntu"; 38 | fontsize="13"; 39 | rankdir="LR"; 40 | bgcolor="aliceblue"; 41 | style="solid"; 42 | penwidth="0.5"; 43 | pad="0.0"; 44 | nodesep="0.35"; 45 | 46 | node [shape="ellipse" style="filled" fillcolor="honeydew" fontname="Ubuntu" penwidth="1.0" margin="0.05,0.0"]; 47 | 48 | {{ range $i, $endpoint := .Endpoints }} 49 | {{printf "subgraph \"cluster_%s\" {" .Endpoint }} 50 | label="{{ .Endpoint }}"; 51 | bgcolor="lightgray"; 52 | shape="box"; 53 | style="solid"; 54 | 55 | "{{ .Endpoint }}" [ shape=record, label="{ { Timeout | {{.Timeout.String}} } | { CacheTTL | {{.CacheTTL.String}} } | { Output | {{.OutputEncoding}} } | { QueryString | {{.QueryString}} } }" ] 56 | {{ if .ExtraConfig }}"extra_{{$i}}" [ shape=record, label="{ {ExtraConfig} {{ range $key, $value := .ExtraConfig }} | { {{ $key }} {{ range $k, $v := $value }}| { {{$k}} | {{$v}} } {{ end }} }{{ end }} }" ]{{ end }} 57 | {{ range $j, $backend := .Backends }} 58 | {{printf "subgraph \"cluster_%s\" {" .URLPattern }} 59 | label="{{ .URLPattern }}"; 60 | bgcolor="beige"; 61 | shape="box"; 62 | style="solid"; 63 | "in_{{$i}}_{{$j}}" [ shape=record, label="{ {sd|{{ if .SD }}{{ .SD }}{{ else }}static{{ end }} } | { Hosts | {{.Host}} } | { Encoding | {{ if .Encoding }}{{ .Encoding }}{{ else }}JSON{{ end }} } }" ] 64 | {{ if .ExtraConfig }}"extra_{{$i}}_{{$j}}" [ shape=record, label="{ { ExtraConfig {{ range $key, $v := .ExtraConfig }} | {{ $key }} {{ end }} } }" ]{{ end }} 65 | {{println "}" }} 66 | "{{ $endpoint.Endpoint }}" -> in_{{$i}}_{{$j}} [ label="x{{ .ConcurrentCalls }}"]{{ end }} 67 | {{ println "}" }}{{ end }} 68 | {{ range .Endpoints }} 69 | ":{{ $port }}" -> "{{ .Endpoint }}" [ label="{{ .Method }}"]{{ end }} 70 | } 71 | ` 72 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "os" 6 | ) 7 | 8 | func runFunc(cmd *cobra.Command, args []string) { 9 | if cfgFilePath == "" { 10 | cmd.Println("Please provide the path to your melody config file ") 11 | return 12 | } 13 | //Parse config file 14 | serviceConfig, err := parser.Parse(cfgFilePath) 15 | if err != nil { 16 | //Show config file parse error and exit 17 | cmd.Printf("ERROR parsing the melody config file: %s\n", err.Error()) 18 | os.Exit(-1) 19 | } 20 | //Judge is debug 21 | serviceConfig.Debug = serviceConfig.Debug || debug 22 | //Run with service config 23 | run(serviceConfig) 24 | } 25 | -------------------------------------------------------------------------------- /config/parser.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | //Parser returns a ServiceConfig struct according to the providing configFile name. 11 | type Parser interface { 12 | Parse(configFile string) (ServiceConfig, error) 13 | } 14 | 15 | // NewParseError returns a new ParseError 16 | func NewParseError(err error, configFile string, offset int) *ParseError { 17 | b, _ := ioutil.ReadFile(configFile) 18 | row, col := getErrorRowCol(b, offset) 19 | return &ParseError{ 20 | ConfigFile: configFile, 21 | Err: err, 22 | Offset: offset, 23 | Row: row, 24 | Col: col, 25 | } 26 | } 27 | 28 | //CheckErr returns error when parse config file 29 | func CheckErr(err error, configFile string) error { 30 | switch e := err.(type) { 31 | case *json.SyntaxError: 32 | return NewParseError(err, configFile, int(e.Offset)) 33 | case *json.UnmarshalTypeError: 34 | return NewParseError(err, configFile, int(e.Offset)) 35 | case *os.PathError: 36 | return fmt.Errorf( 37 | "'%s' (%s): %s", 38 | configFile, 39 | e.Op, 40 | e.Err.Error(), 41 | ) 42 | default: 43 | return fmt.Errorf("'%s': %v", configFile, err) 44 | } 45 | } 46 | 47 | func getErrorRowCol(source []byte, offset int) (row, col int) { 48 | for i := 0; i < offset; i++ { 49 | v := source[i] 50 | if v == '\r' { 51 | continue 52 | } 53 | if v == '\n' { 54 | col = 0 55 | row++ 56 | continue 57 | } 58 | col++ 59 | } 60 | return 61 | } 62 | 63 | // ParseError is an error containing details regarding the row and column where 64 | // an parse error occurred 65 | type ParseError struct { 66 | ConfigFile string 67 | Offset int 68 | Row int 69 | Col int 70 | Err error 71 | } 72 | 73 | // Error returns the error message for the ParseError 74 | func (p *ParseError) Error() string { 75 | return fmt.Sprintf( 76 | "'%s': %v, offset: %v, row: %v, col: %v", 77 | p.ConfigFile, 78 | p.Err.Error(), 79 | p.Offset, 80 | p.Row, 81 | p.Col, 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /config/uri.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | defaultHttp = `http://` 11 | ) 12 | 13 | var ( 14 | endpointURLKeysPattern = regexp.MustCompile(`/\{([a-zA-Z\-_0-9]+)\}`) 15 | hostPattern = regexp.MustCompile(`(https?://)?([a-zA-Z0-9\._\-]+)(:[0-9]{2,6})?/?`) 16 | errInvalidHost = errors.New("invalid host") 17 | ) 18 | 19 | //URIParser defines all method that uri needed 20 | type URIParser interface { 21 | CleanHosts([]string) []string 22 | CleanHost(string) string 23 | CleanPath(string) string 24 | GetEndpointPath(string, []string) string 25 | } 26 | 27 | func NewURIParser() URIParser { 28 | return URI(RoutingPattern) 29 | } 30 | 31 | //URI to implement URIParser 32 | type URI int 33 | 34 | func (U URI) CleanHosts(hosts []string) []string { 35 | var cleans []string 36 | for i := range hosts { 37 | cleans = append(cleans, U.CleanHost(hosts[i])) 38 | } 39 | 40 | return cleans 41 | } 42 | 43 | func (U URI) CleanHost(host string) string { 44 | matches := hostPattern.FindAllStringSubmatch(host, -1) 45 | if len(matches) != 1 { 46 | panic(errInvalidHost) 47 | } 48 | 49 | keys := matches[0][1:] 50 | if keys[0] == "" { 51 | keys[0] = defaultHttp 52 | } 53 | 54 | return strings.Join(keys, "") 55 | } 56 | 57 | // 确保 path 前面有 / 58 | func (U URI) CleanPath(path string) string { 59 | return "/" + strings.TrimPrefix(path, "/") 60 | } 61 | 62 | func (U URI) GetEndpointPath(path string, params []string) string { 63 | endPoint := path 64 | if U == ColonRouterPatternBuilder { 65 | for p := range params { 66 | parts := strings.Split(endPoint, "?") 67 | parts[0] = strings.Replace(parts[0], "{"+params[p]+"}", ":"+params[p], -1) 68 | endPoint = strings.Join(parts, "?") 69 | } 70 | } 71 | 72 | return endPoint 73 | } 74 | -------------------------------------------------------------------------------- /core/melody/backend_factory.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | "context" 5 | "melody/config" 6 | "melody/logging" 7 | circuitbreaker "melody/middleware/melody-circuitbreaker/proxy" 8 | martian "melody/middleware/melody-martian" 9 | metrics "melody/middleware/melody-metrics/gin" 10 | juju "melody/middleware/melody-ratelimit/juju/proxy" 11 | "melody/proxy" 12 | "melody/transport/http/client" 13 | ) 14 | 15 | // NewBackendFactory 创建BackendFactory,实际去请求每一个backend 16 | func NewBackendFactoryWithContext(ctx context.Context, logger logging.Logger, metrics *metrics.Metrics) proxy.BackendFactory { 17 | clientFactory := client.NewHTTPClient 18 | httpRequestExecutor := client.DefaultHTTPRequestExecutor(clientFactory) 19 | backendFactory := func(backend *config.Backend) proxy.Proxy { 20 | return proxy.NewHTTPProxyWithHTTPRequestExecutor(backend, httpRequestExecutor, backend.Decoder) 21 | } 22 | backendFactory = martian.NewBackendFactory(logger, httpRequestExecutor) 23 | backendFactory = juju.BackendFactory(backendFactory) 24 | // 使用断路器 25 | backendFactory = circuitbreaker.BackendFactory(backendFactory, logger) 26 | backendFactory = metrics.NewBackendFactory("backend", backendFactory) 27 | return backendFactory 28 | } 29 | -------------------------------------------------------------------------------- /core/melody/encoding.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | rss "melody/middleware/melody-rss" 5 | xml "melody/middleware/melody-xml" 6 | ) 7 | 8 | // RegisterEncoders 注册额外的编码器 9 | func RegisterEncoders() { 10 | xml.Register() 11 | rss.Register() 12 | } 13 | -------------------------------------------------------------------------------- /core/melody/handler_factory.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | "melody/logging" 5 | botmonitor "melody/middleware/melody-botmonitor/gin" 6 | jose "melody/middleware/melody-jose" 7 | ginjose "melody/middleware/melody-jose/gin" 8 | metrics "melody/middleware/melody-metrics/gin" 9 | juju "melody/middleware/melody-ratelimit/juju/router/gin" 10 | router "melody/router/gin" 11 | ) 12 | 13 | // NewHandlerFactory 返回一个Handler工厂 14 | // 根据不同的EndpointConfig定制Handler 15 | // 这里的Handler旨在处理Endpoint层的逻辑 16 | func NewHandlerFactory(logger logging.Logger, rejecter jose.RejecterFactory, metrics *metrics.Metrics) router.HandlerFactory { 17 | handlerFactory := router.EndpointHandler 18 | handlerFactory = juju.NewRateLimiterMw(handlerFactory) 19 | handlerFactory = ginjose.HandlerFactory(handlerFactory, logger, rejecter) 20 | handlerFactory = botmonitor.New(handlerFactory, logger) 21 | handlerFactory = metrics.NewHTTPHandleFactory(handlerFactory) 22 | return handlerFactory 23 | } 24 | -------------------------------------------------------------------------------- /core/melody/plugin.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | "melody/logging" 5 | ) 6 | 7 | // LoadPlugins 加载并注册插件 8 | func LoadPlugins(folder, pattern string, logger logging.Logger) { 9 | 10 | // TODO load client plugin 11 | 12 | // load server plugin 13 | logger.Info("http handler plugins begin to load.") 14 | // n, err = server.Load( 15 | // folder, 16 | // pattern, 17 | // server.RegisterHandler, 18 | // ) 19 | // if err != nil { 20 | // logger.Warning("loading plugins:", err) 21 | // } 22 | // logger.Info("total http handler plugins loaded:", n) 23 | } 24 | -------------------------------------------------------------------------------- /core/melody/proxy_factory.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | "melody/logging" 5 | jsonschema "melody/middleware/melody-jsonschema" 6 | metrics "melody/middleware/melody-metrics/gin" 7 | "melody/proxy" 8 | ) 9 | 10 | func NewProxyFactory(logger logging.Logger, backend proxy.BackendFactory, metrics *metrics.Metrics) proxy.Factory { 11 | // 完成了默认的ProxyFactory 12 | proxyFactory := proxy.NewDefaultFactory(backend, logger) 13 | proxyFactory = proxy.NewShadowFactory(proxyFactory) 14 | proxyFactory = jsonschema.ProxyFactory(proxyFactory) 15 | proxyFactory = metrics.NewProxyFactory("endpoint", proxyFactory) 16 | return proxyFactory 17 | 18 | } 19 | -------------------------------------------------------------------------------- /core/melody/router_engine.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | "io" 5 | "melody/config" 6 | "melody/logging" 7 | 8 | botmonitor "melody/middleware/melody-botmonitor/gin" 9 | cors "melody/middleware/melody-cors/gin" 10 | httpsecure "melody/middleware/melody-httpsecure/gin" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | // NewEngine 返回一个基于gin的默认Engine 16 | func NewEngine(cfg config.ServiceConfig, logger logging.Logger, gelf io.Writer) *gin.Engine { 17 | if !cfg.Debug { 18 | gin.SetMode(gin.ReleaseMode) 19 | } 20 | engine := gin.New() 21 | engine.Use(gin.LoggerWithConfig(gin.LoggerConfig{Output: gelf}), gin.Recovery()) 22 | 23 | // 默认 重定向全部打开 24 | engine.RedirectTrailingSlash = true 25 | engine.RedirectFixedPath = true 26 | engine.HandleMethodNotAllowed = true 27 | // 注册跨域middleware 28 | if mw := cors.New(cfg.ExtraConfig); mw != nil { 29 | engine.Use(mw) 30 | } 31 | //http secure middleware 32 | if err := httpsecure.Register(cfg.ExtraConfig, engine); err != nil { 33 | logger.Warning(err) 34 | } 35 | //TODO lua register 36 | //botmonitor middleware 37 | botmonitor.Register(cfg, logger, engine) 38 | return engine 39 | } 40 | -------------------------------------------------------------------------------- /core/melody/sd.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "melody/config" 7 | "melody/logging" 8 | consul "melody/middleware/melody-consul" 9 | etcd "melody/middleware/melody-etcd" 10 | "melody/sd" 11 | "melody/sd/dnssrv" 12 | ) 13 | 14 | // RegisterSubscriberFactories registers all the available sd adaptors 15 | // sd = service discovery 16 | func RegisterSubscriberFactories(ctx context.Context, cfg config.ServiceConfig, logger logging.Logger) func(n string, p int) { 17 | // setup the etcdReg if necessary 18 | // etcd Raft 19 | etcdReg, err := etcd.New(ctx, cfg.ExtraConfig) 20 | 21 | if err != nil { 22 | logger.Warning("building the etcd client:", err.Error()) 23 | } 24 | 25 | register := sd.GetRegister() 26 | register.Register("etcd", etcd.SubscriberFactory(ctx, etcdReg)) 27 | // register.Get("etcd")(backend) 会得到此backend中以第一个host为prefix的key的所有value的一个slice 28 | // etcdReg.GetEntries(host[0]) 29 | 30 | // register the dns service discovery 31 | // 同上 register.Get("dns")(backend) 32 | register.Register("dns", dnssrv.SubscriberFactory) 33 | 34 | return func(name string, port int) { 35 | if err := consul.Register(ctx, cfg.ExtraConfig, port, name, logger); err != nil { 36 | logger.Error(fmt.Sprintf("Couldn't register %s:%d in consul: %s", name, port, err.Error())) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/version.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "fmt" 4 | 5 | const ( 6 | // MelodyVersion export the project version. 7 | MelodyVersion = "1.0.0" 8 | // MelodyHeaderKey 9 | MelodyHeaderKey = "X-Melody" 10 | // 11 | ) 12 | 13 | var ( 14 | // MelodyUserAgent setted to backend 15 | MelodyUserAgent = fmt.Sprintf("Melody Version %s", MelodyVersion) 16 | MelodyHeaderValue = fmt.Sprintf("Version %s", MelodyVersion) 17 | ) 18 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generate test coverage statistics for Go packages. 3 | # 4 | # Works around the fact that `go test -coverprofile` currently does not work 5 | # with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909 6 | # 7 | # Usage: script/coverage [--html|--coveralls] 8 | # 9 | # --html Additionally create HTML report and open it in browser 10 | # --coveralls Push coverage statistics to coveralls.io 11 | # 12 | # File taken from https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage 13 | 14 | set -e 15 | 16 | workdir=.cover 17 | profile="$workdir/cover.out" 18 | mode=count 19 | 20 | generate_cover_data() { 21 | rm -rf "$workdir" 22 | mkdir "$workdir" 23 | 24 | for pkg in "$@"; do 25 | f="$workdir/$(echo $pkg | tr / -).cover" 26 | go test -covermode="$mode" -coverprofile="$f" "$pkg" 27 | done 28 | 29 | echo "mode: $mode" >"$profile" 30 | grep -h -v "^mode:" "$workdir"/*.cover >>"$profile" 31 | } 32 | 33 | show_cover_report() { 34 | go tool cover -${1}="$profile" 35 | } 36 | 37 | push_to_coveralls() { 38 | echo "Pushing coverage statistics to coveralls.io" 39 | $GOPATH/bin/goveralls -coverprofile="$profile" -repotoken="o4RsPgcCBCApE0hpTyIK5T5SGqM625l5H" 40 | } 41 | 42 | generate_cover_data $(go list ./... | grep -v /examples/) 43 | show_cover_report func 44 | case "$1" in 45 | "") 46 | ;; 47 | --html) 48 | show_cover_report html ;; 49 | --coveralls) 50 | push_to_coveralls ;; 51 | *) 52 | echo >&2 "error: invalid option: $1"; exit 1 ;; 53 | esac -------------------------------------------------------------------------------- /docs/DNS SRV.md: -------------------------------------------------------------------------------- 1 | # DNS # 2 | 定义:域名系统(服务)协议(DNS)是一种分布式网络目录服务,主要用于域名与 IP 地址的相互转换,以及控制因特网的电子邮件的发送。 3 | 4 | ps:dns系统是一个最终一致性分布式系统,即添加的记录并不会及时生效于每一台服务器,但最终肯定会让每台服务器一致。 5 | 6 | # SRV(service record) # 7 | DNS SRV 是 DNS 记录中的一种,用来查询指定服务的地址。SRV中除了记录服务器的地址,还记录了服务的端口,并且可以设置每个服务地址的优先级和权重。 8 | 9 | SRV 的 DNS 类型代码为33。 10 | SRV 的记录格式为: 11 | ``` 12 | _Service._Proto.Name TTL Class SRV Priority Weight Port Target 13 | 14 | Service: 服务名称,前缀“_”是为防止与DNS Label(普通域名)冲突 15 | Proto: 服务使用的通信协议,TCP、UDP、其它标准协议或者自定义的协议 16 | Name: 提供服务的域名 17 | TTL: 缓存有效时间 18 | Class: 类别 19 | Priority: 该记录的优先级,数值越小表示优先级越高,范围0-65535 20 | Weight: 该记录的权重,数值越高权重越高,范围0-65535 21 | Port: 服务端口号,0-65535 22 | Target: host 地址 23 | ``` 24 | 客户端查询到多条记录的时候,使用优先级最高的记录。对相同优先级的记录,按照权重选择,记录的权重越高,被选择的可能性越高。 25 | 26 | 选择的时候,将所有记录的权重值累加,得到一个选择区间\[0,sum\],每个记录在\[0,sum\]中占据一段连续的、长度为自身权重值区间。然后生成一个\[0,sum\]中的随机数,随机数落在的区间所属的记录就是被选择的记录。 -------------------------------------------------------------------------------- /docs/DNS.md: -------------------------------------------------------------------------------- 1 | # DNS 2 | 参考 https://draveness.me/whys-the-design-dns-udp-tcp 3 | ## 概述 4 | 在绝大多数情况下,DNS 都是使用 UDP 协议进行通信的,DNS 协议在设计之初也推荐我们在进行域名解析时首先使用 UDP,这确实能解决很多需求,但是不能解决全部的问题。 5 | 6 | 实际上,DNS 不仅使用了 UDP 协议,也使用了 TCP 协议 7 | 8 | 对 DNS 协议进行简单的介绍:DNS 查询的类型不止包含 A 记录、CNAME 记录等常见查询,还包含 AXFR 类型的特殊查询,这种特殊查询主要用于 DNS 区域传输,它的作用就是在多个命名服务器之间快速迁移记录,由于查询返回的响应比较大,所以会使用 TCP 协议来传输数据包。 9 | 10 | 区域传输:DNS 主从复制,就是将主 DNS 服务器的解析库复制传送至从 DNS 服务器,进而从服务器就可以进行正向、反向解析了。从服务器向主服务器查询更新数据,保证数据一致性,此为区域传送。也可以说,DNS 区域传输,就是 DNS 主从复制的实现方法,DNS 主从复制是 DNS 区域传输的表现形式。 11 | 12 | RFC 文档中与 UDP/TCP 协议相关内容的总结: 13 | 14 | DNS 在被设计之初就可以同时使用 TCP 和 UDP 协议,对于绝大多数的 DNS 查询来说都会使用 UDP 数据报进行传输,TCP 协议只会在区域传输的场景中使用,其中 UDP 数据包只会传输最大 512 字节的数据,多余的会被截断; 15 | 16 | RFC1123 预测了 DNS 记录中存储的数据会越来越多,同时也第一次显式的指出了发现 UDP 包被截断时应该通过 TCP 协议重试。 17 | 18 | 由于互联网的发展,人们发现 IPv4 已经不够分配了,所以引入了更长的 IPv6,DNS 也进行了协议上的支持,随后又增加了在鉴权和安全方面的支持,但是也带来了巨大的 DNS 记录,UDP 数据包被截断变得非常常见。 19 | 20 | RFC7766 才终于提出了使用 TCP 协议作为主要协议来解决 UDP 无法解决的问题,TCP 协议也不再只是一种重试时使用的机制,随后出现的 DNS over TLS 和 DNS over HTTP 也都是对 DNS 协议的一种补充。 21 | 22 | ## 为什么小数据包用 UDP? 23 | 24 | 以前的 DNS 查询的数据包较小、机制简单; 25 | 26 | UDP 协议的额外开销小、有着更好的性能表现,而 TCP 需要三次握手,相对于当时的小数据包来说是很大的开销; 27 | 28 | ## 为什么大数据包用 TCP? 29 | 30 | DNS 查询由于 DNSSEC 和 IPv6 的引入迅速膨胀,导致 DNS 响应经常超过 MTU(传送链路的最大传输单元,也就是单个数据包大小的上限,一般为 1500 字节) 造成数据的分片和丢失,我们需要依靠更加可靠的 TCP 协议(通过序列号、重传等机制能够保证消息的不重不漏,消息接受方的 TCP 栈会对分片的数据重新进行拼装,DNS 等应用层协议可以直接使用处理好的完整数据)完成数据的传输; 31 | 32 | 随着 DNS 查询中包含的数据不断增加,TCP 协议头以及三次握手带来的额外开销比例逐渐降低,不再是占据总传输数据大小的主要部分; 33 | -------------------------------------------------------------------------------- /docs/ETCD使用介绍.md: -------------------------------------------------------------------------------- 1 | # ETCD # 2 | [中文文档](https://github.com/doczhcn/etcd/blob/master/documentation/index.md) - 翻译自ETCD官方文档 3 | ## 简介 ## 4 | etcd 是一个**分布式键值对存储**,设计用来可靠而快速的保存关键数据并提供访问。通过分布式锁,leader选举和写屏障(write barriers)来实现可靠的分布式协作。etcd集群是为高可用,**持久性数据存储和检索**而准备。 5 | 6 | 注意到 etcd 本质上其实是个分布式的数据库,只不过它可以保证每一台机器上的数据是一致的! 7 | 8 | 例如启动 mysql server 一样,etcd 也需要开启 etcd server。 9 | 10 | etcd client 用来对 etcd server 进行数据的 CRUD。 -------------------------------------------------------------------------------- /docs/HTTP公钥固定.md: -------------------------------------------------------------------------------- 1 | # HTTP公钥固定(Public Key Pinning) 2 | 3 | ## 什么是HPKP? 4 | 5 | HTTP公钥固定(HPKP)是一种安全功能,它告诉Web客户端将特定加密公钥与某个Web服务器相关联,以降低使用伪造证书进行“MITM攻击(中间人攻击)”的风险。 6 | 7 | 为了确保TLS会话中使用的服务器公钥的真实性,此公钥将包装到X.509证书中,该证书通常由证书颁发机构(CA)签名。诸如浏览器之类的Web客户端信任许多这些CA,它们都可以为任意域名创建证书。如果攻击者能够破坏单个CA,则他们可以对各种TLS连接执行MITM攻击。 HPKP可以通过告知客户端哪个公钥属于某个Web服务器来规避HTTPS协议的威胁。 8 | 9 | HPKP是首次使用信任(TOFU)技术。 Web服务器第一次通过特殊的HTTP头告诉客户端哪些公钥属于它,客户端将该信息存储在给定的时间段内。当客户端再次访问服务器时,它希望证书链中至少有一个证书包含一个公钥,其指纹已通过HPKP获知。如果服务器提供未知的公钥,则客户端应向用户发出警告。 10 | 11 | ## 如何启用HPKP? 12 | 13 | 要为您的站点启用此功能,您需要在通过HTTPS访问站点时返回Public-Key-Pins HTTP标头: 14 | ```shell script 15 | Public-Key-Pins: pin-sha256="base64=="; max-age=expireTime [; includeSubDomains][; report-uri="reportURI"] 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/JWT.md: -------------------------------------------------------------------------------- 1 | # JWT(JSON Web Token) # 2 | 传统的 session、cookie 验证用户登录状态不便于分布式集群系统。(服务器有状态) 3 | 4 | JWT 可以轻松的在分布式集群系统中解决验证登录态的问题。(服务器无状态) 5 | 6 | JWT 是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户。 7 | 8 | 之后,当用户与服务器通信时,客户在请求中发回JSON对象。 9 | 服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。 10 | 11 | 服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。 12 | 13 | 一个 JWT 实际上就是一个字符串,它由三部分组成,**头部**、**有效载荷**与**签名**。 14 | 15 | ## 头部(Header) ## 16 | 头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。 17 | ```json 18 | { 19 | "typ": "JWT", 20 | "alg": "HS256" 21 | } 22 | ``` 23 | 在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256); 24 | typ属性表示令牌的类型,JWT令牌统一写为JWT。 25 | 最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。 26 | 27 | ## 有效载荷(Payload) ## 28 | 有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。这些有效信息包含三个部分: 29 | 30 | (1) 七个默认字段供选择。(建议但不强制使用) 31 | 32 | ``` 33 | iss: jwt签发者 34 | sub: jwt所面向的用户 35 | aud: 接收jwt的一方 36 | exp: jwt的过期时间,这个过期时间必须要大于签发时间 37 | nbf: 定义在什么时间之前,该jwt都是不可用的. 38 | iat: jwt的签发时间 39 | jti: jwt的唯一身份标识,主要用来作为一次性token。 40 | ``` 41 | (2) 公共的声明 42 | 43 | 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息. 44 | 但不建议添加敏感信息,因为该部分在客户端可解密. 45 | 46 | (3) 私有的声明 47 | 48 | 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息。 49 | 50 | 有效载荷的JSON对象也使用Base64 URL算法转换为字符串保存。 51 | 52 | ## 签名(Signature) ## 53 | JWT 的第三部分是一个签证信息,这个签证信息由三部分组成: 54 | ``` 55 | Header (base64URL后的) 56 | Payload (base64URL后的) 57 | Secret 58 | ``` 59 | 这个部分需要Header和Payload使用`.`连接组成的字符串,然后通过Header中声明的加密方式进行加盐Secret组合加密,然后就构成了JWT的第三部分。 60 | 61 | **注意**:Secret是保存在服务器端的,JWT的签发生成也是在服务器端的,Secret就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个Secret, 那就意味着客户端是可以自我签发JWT了。 62 | -------------------------------------------------------------------------------- /docs/MIME嗅探防御.md: -------------------------------------------------------------------------------- 1 | # MIME sniffing(MIME 嗅探) 2 | 3 | 产生这个问题的主要原因是,http报文中的content-type头在早期版本的浏览器中(例如IE7/6),浏览器并不光会凭靠content-type头进行不同类型的解析,还会`对response内容进行自我解析`,例如text/plain将文件内容直接显示,jpeg格式进行jpeg的渲染,例如传输的content-type头格式为 text/plain ,将文件内容直接显示,response中内容为 4 | ``` 5 | 6 | ``` 7 | IE浏览器会将其自动嗅探,并认为是text/html类型,并执行相应的渲染逻辑,这就很容易引发XSS攻击。 攻击者可以将图片文件内容写入xss攻击语句,并上传到共享的网站上,当用户请求该文件时,老旧浏览器收到response进行违背content-type的html解析,从而触发MIME嗅探攻击,我所知的解决方法为资源服务器使用与主网站不同的域名,在同源政策下,阻止跨域请求并解析的行为,从而达到阻止这一类嗅探攻击的目的。 -------------------------------------------------------------------------------- /docs/Melody Server.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/Melody Server.xmind -------------------------------------------------------------------------------- /docs/VuePress入门使用.md: -------------------------------------------------------------------------------- 1 | # VuePress 搭建文档手册 2 | 3 | ## 前提 4 | 5 | - node 环境 6 | - npm 7 | 8 | ## 安装 9 | 10 | ``` 11 | npm install -g vuepress 12 | ``` 13 | 14 | ## 开始 15 | 16 | 1. 新建个文件夹 17 | 2. 初始化,生成pakcage.json 18 | ``` 19 | npm init -y 20 | ``` 21 | 3. vscode打开项目 22 | 4. 新建`docs`目录 23 | 5. `docs下`新建`.vuepress`目录 24 | 6. `.vuepress`下新建`config.json` 25 | ```config.json 26 | module.exports = { 27 | title: "Melody Docs", 28 | description: "Documents of melody" 29 | } 30 | ``` 31 | 7. `docs`下新建`README.md` 32 | 8. 尝试启动 33 | ``` 34 | sudo vuepress dev docs 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/img/example.png -------------------------------------------------------------------------------- /docs/img/melody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/img/melody.png -------------------------------------------------------------------------------- /docs/img/min-melody-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/img/min-melody-logo.png -------------------------------------------------------------------------------- /docs/img/p-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/img/p-1.png -------------------------------------------------------------------------------- /docs/img/p-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/img/p-2.png -------------------------------------------------------------------------------- /docs/img/p-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/img/p-3.png -------------------------------------------------------------------------------- /docs/img/struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipped-aurora/melody/13a529e1e41c1b5eb567bde2196fe199ae958584/docs/img/struct.png -------------------------------------------------------------------------------- /docs/questions.md: -------------------------------------------------------------------------------- 1 | 1. etcd在网关中是具体用来做什么的?为什么只取backends\[0\]作为machine? 2 | 2. dns srv记录具体是用来做什么的?本地如何添加srv记录? 3 | 3. consul是来监听bloomfilter rpc server 的吗? 4 | 4. rotate bloomfilter 比 普通的 bloomfilter好在哪里?为什么这么设计?有什么是rotate可以完成,而普通的不能完成的? 5 | 5. rpc input 为什么要用 [][]byte 而不是 []byte? -------------------------------------------------------------------------------- /docs/令牌桶算法.md: -------------------------------------------------------------------------------- 1 | ## Token_bucket 令牌桶算法简介 2 | 3 | ### 一、情景和问题 4 | 5 | ​ 某天我们突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。 6 | 7 | 8 | 9 | ### 二、令牌桶算法 10 | 11 | [令牌桶算法](https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95)是[网络流量](https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E6%B5%81%E9%87%8F)整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,[令牌桶算法](https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95)用来控制发送到网络上的数据的数目,并允许突发数据的发送。 12 | 13 | 令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节(对于流量整形来说代表一个bit,就traffic policing来讲代表一个byte。参见CCIE Routing and Switching Official Exam Certification Guide 2nd Edition)。如果令牌桶中存在令牌,则允许发送流量;而如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。 14 | 15 | 如图所示:随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token,如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务. 16 | 17 | ![1582702574508](http://picture.zyuhn.top/myblog/promise/20200226153614-545968.png) 18 | 19 | 令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量. 20 | 21 | 22 | 23 | ### 三、算法文字描述 24 | 25 | 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中; 26 | 27 | 假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃; 28 | 29 | 当一个n个字节的[数据包](https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%8C%85)到达时,就从令牌桶中删除n个令牌,并且数据包被发送到网络; 30 | 31 | 如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在[流量限制](https://baike.baidu.com/item/%E6%B5%81%E9%87%8F%E9%99%90%E5%88%B6)之外; 32 | 33 | 算法允许最长b个字节的突发,但从长期运行结果看,[数据包](https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%8C%85)的速率被限制成常量r。对于在[流量限制](https://baike.baidu.com/item/%E6%B5%81%E9%87%8F%E9%99%90%E5%88%B6)外的[数据包](https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%8C%85)可以以不同的方式处理: 34 | 35 | 它们可以被丢弃; 36 | 37 | 它们可以排放在[队列](https://baike.baidu.com/item/%E9%98%9F%E5%88%97)中以便当令牌桶中累积了足够多的令牌时再传输; 38 | 39 | 它们可以继续发送,但需要做特殊标记,网络过载的时候将这些特殊标记的包丢弃。 40 | 41 | 注意:[令牌桶算法](https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95)不能与另外一种常见算法“[漏桶算法](https://baike.baidu.com/item/%E6%BC%8F%E6%A1%B6%E7%AE%97%E6%B3%95)(Leaky Bucket)”相混淆。这两种算法的主要区别在于“[漏桶算法](https://baike.baidu.com/item/%E6%BC%8F%E6%A1%B6%E7%AE%97%E6%B3%95)”能够强行限制数据的传输速率,而“[令牌桶算法](https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95)”在能够限制数据的平均传输数据外,还允许某种程度的突发传输。在“[令牌桶算法](https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95)”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。 -------------------------------------------------------------------------------- /docs/关于HTTP严格传输安全.md: -------------------------------------------------------------------------------- 1 | # HTTP Strict Transport Security (HSTS) - HTTP严格传输安全 2 | 3 | ## 1. 由来:启用HTTPS也不能保证足够安全 4 | 5 | 很多网站对外只开放HTTPS,但用户再访问某个网站的时候,在浏览器中却往往只输入网站域名,例如(example.com), 而不是(https://example.com),不过浏览器依然能正确的使用HTTPS发起请求,这个是由于浏览器在背后的协作。 6 | 7 | 简单说就是浏览器向server发起了一次HTTP请求,在得到一个重定向响应后,发起一次HTTPS请求并得到最终的响应内容,这看起来是个不错的用户体验。 8 | 9 | 但在建立起HTTPS连接之前存在一次明文的HTTP请求和重定向,使得攻击者可以以中间人的方式劫持这次请求,从而进行后续的攻击,例如例如窃听、篡改、跳转钓鱼网站等。 10 | 11 | #### 劫持请求并跳转钓鱼网站为例 12 | 13 | 1. 浏览器发起一次明文HTTP请求,但实际会被攻击者拦截下来 14 | 2. 攻击者作为代理,把当前请求转发到钓鱼王占 15 | 3. 钓鱼网站返回假冒的网页内容 16 | 4. 攻击者吧假冒的网页内容返回给浏览器 17 | 18 | 是整个过程中,攻击者直接劫持了HTTP请求,并返回内容给浏览器,根本不给浏览器建立HTTPS连接的机会,因此浏览器会误以为真是网站通过HTTP对外提供服务,自然就不会向用户报告当前的连接不安全,于是攻击者可以对请求和响应进行窃听和篡改。 19 | 20 | ## 2. 解决方法:使用HSTS 21 | 22 | 劫持出现在HTTPS连接前的HTTP连接,那么解决这个问题的思路自然就变成了如何避免出现这样的HTTP请求,我们期望的浏览器行为是:用户输入域名(example.com),浏览器直接将其转换为HTTPS请求(https://example.com),直接略过上述的HTTP请求和重定向,从而使得中间人攻击失效 23 | 24 | #### 期望行为 25 | 26 | 1. 用户在浏览器输入域名,浏览器得知该域名应该使用HTTPS进行通信 27 | 2. 浏览器直接向网站发起HTTPS请求 28 | 3. 网站返回相应内容 29 | 30 | 但是浏览器如何知道该域名应该是用HTTPS通信呢? 31 | 32 | ### 2.1 HSTS 33 | 34 | HSTS全称是HTTP Strict Transport Security, 他是一个Web安全策略机制(web security policy mechanism) 35 | 36 | HSTS最为核心的是一个HTTP响应头(HTTP Response Header)。通过这响应头让浏览器得知,在接下来的一段时间内,当前域名只能通过HTTPS进行访问,并且在浏览器发现当前连接不安全的情况下,强行拒绝用户的后续访问要求 37 | 38 | HSTS Header的语法: 39 | 40 | ``` 41 | Strict-Transport-Security: [; includeSubDomains][; preload] 42 | ``` 43 | 44 | 其中 45 | 46 | - max-age 必填参数,秒为单位的数值,代表着HSTS Header的过期时间,通常设置为1年,即31536000秒 47 | - includeSubDomains 可选参数,如果包含,则意味着当前域名机器子域名均开启HSTS保护 48 | - preload 可选参数,只有当你申请将自己的域名加入到浏览器内置列表的时候才需要使用到它 49 | 50 | #### 让浏览器直接发送HTTPS请求 51 | 52 | 只需要在响应头中加入 53 | ``` 54 | Strict-Transport-Security: max-age=31536000; includeSubDomains 55 | ``` 56 | 57 | 就可以告诉浏览器该域名或子域名在接下来的一年内,对其应强制性使用HTTPS 58 | 59 | 60 | 那么如果有效期过了怎么办? 其实HSTS Header存在于每个响应之中,随着用户和网站的交互,这个有效时间时刻都在刷新,再加上有效期通常被设置为1年,所以只要两次请求间隔小于一年,基本上不会出现风险。 61 | 62 | ## 3. 攻击者依然有机可乘 63 | 64 | 但是还有个问题,HSTS Header存在于响应头中,要拿到响应头还是得发送一次HTTP请求,在第一次访问网站的时候,这时候中间人依然可以将这个HTTP请求劫持下来,继续中间人攻击。 65 | 66 | ## 4. 防御到底 67 | 68 | 针对上面的情况,HSTS也作出了应对, 在浏览器中内置一个列表,只要是在这个列表里的域名,无论何时、何种情况,浏览器都只使用HTTPS发起连接,这个列表由Google Chromium维护,主流的浏览器都在使用 -------------------------------------------------------------------------------- /docs/点劫持保护.md: -------------------------------------------------------------------------------- 1 | # 点劫持保护 2 | 3 | 点击劫持中间件和装饰器提供了简捷易用的,对点击劫持的保护。这种攻击在恶意站点诱导用户点击另一个站点的被覆盖元素时出现,另一个站点已经加载到了隐藏的frame或iframe中。 4 | 5 | ## 示例 6 | 7 | 假设一个在线商店拥有一个页面,已登录的用户可以点击“现在购买”来购买一个商品。用户为了方便,可以选择一直保持商店的登录状态。一个攻击者的站点可能在他们自己的页面上会创建一个“我喜欢Ponies”的按钮,并且在一个透明的iframe中加载商店的页面,把“现在购买”的按钮隐藏起来覆盖在“我喜欢Ponies”上。如果用户访问了攻击者的站点,点击“我喜欢Ponies”按钮会触发对“现在购买”按钮的无意识的点击,不知不觉中购买了商品。 8 | 9 | ## 防御 10 | 11 | 现代浏览器遵循X-Frame-Options协议头,它表明一个资源是否允许加载到frame或者iframe中。如果响应包含值为SAMEORIGIN的协议头,浏览器会在frame中只加载同源请求的的资源。如果协议头设置为DENY,浏览器会在加载frame时屏蔽所有资源,无论请求来自于哪个站点。 12 | 13 | ## 支持的浏览器 14 | 15 | - Internet Explorer 8+ 16 | - Firefox 3.6.9+ 17 | - Opera 10.5+ 18 | - Safari 4+ 19 | - Chrome 4.1+ -------------------------------------------------------------------------------- /docs/相关博客链接.md: -------------------------------------------------------------------------------- 1 | 2 | **英文文献:** 3 | 4 | [How-to-Choose-The-Right-API-Gateway-For-Your-Platform](https://www.moesif.com/blog/technical/api-gateways/How-to-Choose-The-Right-API-Gateway-For-Your-Platform-Comparison-Of-Kong-Tyk-Apigee-And-Alternatives/) 5 | 6 | **中文文献:** 7 | 8 | [谈谈微服务中的API网关](https://www.cnblogs.com/savorboard/p/api-gateway.html) 9 | 10 | [API网关在API安全性中的作用](https://www.jianshu.com/p/0cc79fe3e617) 11 | 12 | [基于HTTP反向代理的Web安全网关技术研究](http://kns.cnki.net/KCMS/detail/detail.aspx?filename=1016290366.nh&dbname=CMFD201701&dbcode=cdmd&uid=&v=MDkxODRHTEd4SHRMS3FaRWJQSVIrZm5zNHlSWWFtejExUEhia3FXQTBGckNVUjdxZlplUnRGeURuV3I3T1ZGMjY=) - 来源知网:其中在该文献页面中可以找到相关的英文或者中文资料(相似文献或参考文献) 13 | 14 | [微服务:监控体系,容器监控](https://blog.csdn.net/fly910905/article/details/100059983?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task) 15 | 16 | [基于Raft一致性协议的高可用性实现](https://kns.cnki.net/kcms/detail/detail.aspx?filename=HDSZ201505016&dbcode=CJFQ&dbname=CJFD2015&v=) 17 | 18 | [API 网关知识看这篇就足够了!](https://blog.csdn.net/qq_34337272/article/details/102962588) 19 | 20 | [京东10亿级调用量背后的高可用网关系统架构实践!](http://www.yunweipai.com/archives/23653.html) 21 | 22 | 23 | **其他网站:** 24 | 25 | [搜论文、期刊](http://www.wanfangdata.com.cn/index.html) - 帐号密码均为 hhgxy2020 26 | 27 | [krakend](https://www.krakend.io/) 28 | 29 | [kong](https://konghq.com/) 30 | 31 | **使用文档:** 32 | 33 | [InfluxDB 中文文档](https://jasper-zhang1.gitbooks.io/influxdb/) 34 | 35 | [InfluxDB 学习、语法](https://www.linuxdaxue.com/influxdb-continuous-queries.html) 36 | -------------------------------------------------------------------------------- /encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "io" 4 | 5 | const NOOP = "no-op" 6 | 7 | // NoOpDecoder implements the Decoder interface 8 | func NoOpDecoder(_ io.Reader, _ *map[string]interface{}) error { return nil } 9 | 10 | func NoOpDecoderFactory(_ bool) func(io.Reader, *map[string]interface{}) error { return NoOpDecoder } 11 | 12 | // Decoder a accept a param that can be read , and a target map pointer to write 13 | type Decoder func(io.Reader, *map[string]interface{}) error 14 | 15 | // DecoderFactory returns a Decoder 16 | // 1. EntityDecoder {} 17 | // 2. CollectionDecoder [] 18 | type DecoderFactory func(bool) func(io.Reader, *map[string]interface{}) error 19 | 20 | func Get(key string) DecoderFactory { 21 | return decoders.Get(key) 22 | } 23 | 24 | func Register(key string, factory func(bool) func(io.Reader, *map[string]interface{}) error) error { 25 | return decoders.Register(key, factory) 26 | } 27 | -------------------------------------------------------------------------------- /encoding/json.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | const JSON = "json" 9 | 10 | func NewJSONDecoder(isCollection bool) func(io.Reader, *map[string]interface{}) error { 11 | if isCollection { 12 | return JSONCollectionDecoder() 13 | } else { 14 | return JSONDecoder() 15 | } 16 | } 17 | 18 | func JSONDecoder() Decoder { 19 | return func(reader io.Reader, i *map[string]interface{}) error { 20 | d := json.NewDecoder(reader) 21 | d.UseNumber() 22 | return d.Decode(i) 23 | } 24 | } 25 | 26 | func JSONCollectionDecoder() Decoder { 27 | return func(reader io.Reader, i *map[string]interface{}) error { 28 | var collection []interface{} 29 | d := json.NewDecoder(reader) 30 | d.UseNumber() 31 | if err := d.Decode(&collection); err != nil { 32 | return err 33 | } 34 | *i = map[string]interface{}{"list": collection} 35 | return nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /encoding/register.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "io" 5 | "melody/register" 6 | ) 7 | 8 | var ( 9 | decoders = initDecoderRegister() 10 | defaultDecoders = map[string]func(bool) func(io.Reader, *map[string]interface{}) error{ 11 | JSON: NewJSONDecoder, 12 | STRING: NewStringDecoder, 13 | NOOP: NoOpDecoderFactory, 14 | } 15 | ) 16 | 17 | type DecoderRegister struct { 18 | data register.Untyped 19 | } 20 | 21 | func (d *DecoderRegister) Get(s string) DecoderFactory { 22 | // if can not get decoder:key = s, 23 | // return json decoder 24 | for _, v := range []string{s, JSON} { 25 | if v, ok := d.data.Get(v); ok { 26 | decoderFactory, ok := v.(func(bool) func(io.Reader, *map[string]interface{}) error) 27 | if ok { 28 | return decoderFactory 29 | } 30 | } 31 | } 32 | 33 | return NewJSONDecoder 34 | } 35 | 36 | func (d *DecoderRegister) Register(name string, factory func(bool) func(io.Reader, *map[string]interface{}) error) error { 37 | d.data.Register(name, factory) 38 | return nil 39 | } 40 | 41 | func initDecoderRegister() *DecoderRegister { 42 | decoder := &DecoderRegister{data: register.New()} 43 | for k, v := range defaultDecoders { 44 | decoder.data.Register(k, v) 45 | } 46 | return decoder 47 | } 48 | -------------------------------------------------------------------------------- /encoding/string.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | ) 7 | 8 | const STRING = "string" 9 | 10 | func NewStringDecoder(_ bool) func(io.Reader, *map[string]interface{}) error { 11 | return StringDecoder() 12 | } 13 | 14 | func StringDecoder() Decoder { 15 | return func(reader io.Reader, i *map[string]interface{}) error { 16 | data, err := ioutil.ReadAll(reader) 17 | if err != nil { 18 | return err 19 | } 20 | *i = map[string]interface{}{"content": string(data)} 21 | return nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // Logger show logging information with level 13 | type Logger interface { 14 | Debug(v ...interface{}) 15 | Info(v ...interface{}) 16 | Warning(v ...interface{}) 17 | Error(v ...interface{}) 18 | Critical(v ...interface{}) 19 | Fatal(v ...interface{}) 20 | } 21 | 22 | const ( 23 | // LEVEL_DEBUG = 0 24 | LEVEL_DEBUG = iota 25 | // LEVEL_INFO = 1 26 | LEVEL_INFO 27 | // LEVEL_WARNING = 2 28 | LEVEL_WARNING 29 | // LEVEL_ERROR = 3 30 | LEVEL_ERROR 31 | // LEVEL_CRITICAL = 4 32 | LEVEL_CRITICAL 33 | ) 34 | 35 | var ( 36 | ErrInvalidLogLevel = fmt.Errorf("invalid log level") 37 | defaultLogger = logger{Level: LEVEL_CRITICAL, Prefix: ""} 38 | logLevels = map[string]int{ 39 | "DEBUG": LEVEL_DEBUG, 40 | "INFO": LEVEL_INFO, 41 | "WARNING": LEVEL_WARNING, 42 | "ERROR": LEVEL_ERROR, 43 | "CRITICAL": LEVEL_CRITICAL, 44 | } 45 | // NoOp is the NO-OP logger 46 | NoOp, _ = NewLogger("CRITICAL", ioutil.Discard, "") 47 | ) 48 | 49 | //NewLogger returns a base logger 50 | func NewLogger(level string, out io.Writer, prefix string) (Logger, error) { 51 | log.SetOutput(out) 52 | l, ok := logLevels[strings.ToUpper(level)] 53 | if !ok { 54 | return defaultLogger, ErrInvalidLogLevel 55 | } 56 | return logger{Level: l, Prefix: prefix}, nil 57 | } 58 | 59 | type logger struct { 60 | Level int 61 | Prefix string 62 | } 63 | 64 | // Debug logs a message using DEBUG as log level. 65 | func (l logger) Debug(v ...interface{}) { 66 | if l.Level > LEVEL_DEBUG { 67 | return 68 | } 69 | l.prependLog("DEBUG:", v) 70 | } 71 | 72 | // Info logs a message using INFO as log level. 73 | func (l logger) Info(v ...interface{}) { 74 | if l.Level > LEVEL_INFO { 75 | return 76 | } 77 | l.prependLog("INFO:", v) 78 | } 79 | 80 | // Warning logs a message using WARNING as log level. 81 | func (l logger) Warning(v ...interface{}) { 82 | if l.Level > LEVEL_WARNING { 83 | return 84 | } 85 | l.prependLog("WARNING:", v) 86 | } 87 | 88 | // Error logs a message using ERROR as log level. 89 | func (l logger) Error(v ...interface{}) { 90 | if l.Level > LEVEL_ERROR { 91 | return 92 | } 93 | l.prependLog("ERROR:", v) 94 | } 95 | 96 | // Critical logs a message using CRITICAL as log level. 97 | func (l logger) Critical(v ...interface{}) { 98 | l.prependLog("CRITICAL:", v) 99 | } 100 | 101 | // Fatal is equivalent to l.Critical(fmt.Sprint()) followed by a call to os.Exit(1). 102 | func (l logger) Fatal(v ...interface{}) { 103 | l.prependLog("FATAL:", v) 104 | os.Exit(1) 105 | } 106 | 107 | func (l logger) prependLog(level string, v []interface{}) { 108 | log.Println(append([]interface{}{l.Prefix, level}, v...)...) 109 | } 110 | -------------------------------------------------------------------------------- /logging/log_test.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | const ( 12 | debugMsg = "Debug msg" 13 | infoMsg = "Info msg" 14 | warningMsg = "Warning msg" 15 | errorMsg = "Error msg" 16 | criticalMsg = "Critical msg" 17 | fatalMsg = "Fatal msg" 18 | ) 19 | 20 | func TestNewLogger(t *testing.T) { 21 | levels := []string{"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} 22 | regexps := []*regexp.Regexp{ 23 | regexp.MustCompile(debugMsg), 24 | regexp.MustCompile(infoMsg), 25 | regexp.MustCompile(warningMsg), 26 | regexp.MustCompile(errorMsg), 27 | regexp.MustCompile(criticalMsg), 28 | } 29 | 30 | for i, level := range levels { 31 | output := logSomeStuff(level) 32 | for j := i; j < len(regexps); j++ { 33 | if !regexps[j].MatchString(output) { 34 | t.Errorf("The output doesn't contain the expected msg for the level: %s. [%s]", level, output) 35 | } 36 | } 37 | } 38 | } 39 | 40 | func TestNewLogger_unknownLevel(t *testing.T) { 41 | _, err := NewLogger("UNKNOWN", bytes.NewBuffer(make([]byte, 1024)), "pref") 42 | if err == nil { 43 | t.Error("The factory didn't return the expected error") 44 | return 45 | } 46 | if err != ErrInvalidLogLevel { 47 | t.Errorf("The factory didn't return the expected error. Got: %s", err.Error()) 48 | } 49 | } 50 | 51 | func TestNewLogger_fatal(t *testing.T) { 52 | if os.Getenv("BE_CRASHER") == "1" { 53 | l, err := NewLogger("Critical", bytes.NewBuffer(make([]byte, 1024)), "pref") 54 | if err != nil { 55 | t.Error("The factory returned an expected error:", err.Error()) 56 | return 57 | } 58 | l.Fatal("crash!!!") 59 | return 60 | } 61 | cmd := exec.Command(os.Args[0], "-test.run=TestNewLogger_fatal") 62 | cmd.Env = append(os.Environ(), "BE_CRASHER=1") 63 | err := cmd.Run() 64 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 65 | return 66 | } 67 | t.Fatalf("process ran with err %v, want exit status 1", err) 68 | } 69 | 70 | func logSomeStuff(level string) string { 71 | buff := bytes.NewBuffer(make([]byte, 1024)) 72 | logger, _ := NewLogger(level, buff, "pref") 73 | 74 | logger.Debug(debugMsg) 75 | logger.Info(infoMsg) 76 | logger.Warning(warningMsg) 77 | logger.Error(errorMsg) 78 | logger.Critical(criticalMsg) 79 | 80 | return buff.String() 81 | } 82 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "melody/cmd" 7 | "melody/core/melody" 8 | viper "melody/middleware/melody-viper" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | ) 13 | 14 | func main() { 15 | sigs := make(chan os.Signal, 1) 16 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | go func() { 21 | select { 22 | case sig := <-sigs: 23 | log.Println("Signal intercepted:", sig) 24 | cancel() 25 | case <-ctx.Done(): 26 | } 27 | }() 28 | 29 | melody.RegisterEncoders() 30 | 31 | parser := viper.New() 32 | cmd.Execute(parser, melody.NewExecutor(ctx)) 33 | } 34 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCommandRun(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /melody-cases/Makefile: -------------------------------------------------------------------------------- 1 | p: 2 | go run profile/main.go 3 | 4 | r: 5 | go run role/main.go 6 | 7 | c: 8 | go run coupon/main.go 9 | 10 | f: 11 | go run finance/main.go -------------------------------------------------------------------------------- /melody-cases/coupon/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | profileEngine := gin.Default() 11 | 12 | profileEngine.GET("/coupon/:uuid", func(context *gin.Context) { 13 | uuid := context.Param("uuid") 14 | context.JSON(http.StatusOK, map[string]interface{}{ 15 | "id": 1, 16 | "uuid": uuid, 17 | "coupons": []map[string]interface{}{ 18 | { 19 | "title": "5元优惠券", 20 | "condition": "age > 20", 21 | "start": "xxxx-xx-xx", 22 | "end": "xxxx-xx-xx", 23 | }, 24 | { 25 | "title": "7元优惠券", 26 | "condition": "none", 27 | "start": "xxxx-xx-xx", 28 | "end": "xxxx-xx-xx", 29 | }, 30 | { 31 | "title": "7元优惠券", 32 | "condition": "none", 33 | "start": "xxxx-xx-xx", 34 | "end": "xxxx-xx-xx", 35 | }, 36 | { 37 | "title": "5元优惠券", 38 | "condition": "vip true", 39 | "start": "xxxx-xx-xx", 40 | "end": "xxxx-xx-xx", 41 | }, 42 | }, 43 | }) 44 | }) 45 | fmt.Println(profileEngine.Run(":9003")) 46 | } 47 | -------------------------------------------------------------------------------- /melody-cases/finance/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | profileEngine := gin.Default() 11 | 12 | profileEngine.GET("/finance/:uuid", func(context *gin.Context) { 13 | uuid := context.Param("uuid") 14 | context.JSON(http.StatusOK, map[string]interface{}{ 15 | "id": 1, 16 | "uuid": uuid, 17 | "name": "Grant", 18 | "level": "Gold member", 19 | "active_balance": 5000.22, 20 | "frozen_balance": 1000.00, 21 | "borrow_balance": 50000.00, 22 | }) 23 | }) 24 | fmt.Println(profileEngine.Run(":9004")) 25 | } 26 | -------------------------------------------------------------------------------- /melody-cases/melody.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "output_encoding": "json", 4 | "disable_rest": false, 5 | "extra_config": { 6 | "melody_gologging": { 7 | "prefix": "[演示]", 8 | "level": "DEBUG", 9 | "format": "default", 10 | "syslog": false, 11 | "stdout": true 12 | }, 13 | "melody_metrics": { 14 | "endpoint_disabled": false, 15 | "proxy_disabled": false, 16 | "router_disabled": false, 17 | "backend_disabled": false, 18 | "collection_time": "10s" 19 | }, 20 | "melody_influxdb": { 21 | "address": "http://127.0.0.1:8086", 22 | "username": "", 23 | "password": "", 24 | "db": "demo", 25 | "buffer_size": 1024, 26 | "ttl": "5s", 27 | "time_out": "1s", 28 | "data_server_enable": true, 29 | "data_server_port": ":8001", 30 | "data_server_query_enable": true 31 | }, 32 | "melody_alert": { 33 | "NumGC": "100", 34 | "NumGoroutine": "100", 35 | "Sys": "200m", 36 | "HeapSys": "150m", 37 | "StackSys": "50m", 38 | "MCacheSys": "50m", 39 | "MSpanSys": "20m" 40 | } 41 | }, 42 | "endpoints": [ 43 | { 44 | "endpoint": "/user/info/{id}", 45 | "method": "POST", 46 | "output_encoding": "JSON", 47 | "extra_config": { 48 | "melody_proxy": { 49 | "sequential": true 50 | }, 51 | "melody_alert": { 52 | "time": "10ms", 53 | "size": "1024" 54 | } 55 | }, 56 | "backends": [ 57 | { 58 | "url_pattern": "/profile/{id}", 59 | "method": "GET", 60 | "host": [ 61 | "127.0.0.1:9001" 62 | ], 63 | "encoding": "json", 64 | "sd": "static", 65 | "group": "profile_info" 66 | }, 67 | { 68 | "url_pattern": "/role/{resp0_profile_info.role_id}", 69 | "method": "GET", 70 | "host": [ 71 | "127.0.0.1:9002" 72 | ], 73 | "encoding": "json", 74 | "sd": "static", 75 | "group": "role_info" 76 | } 77 | ], 78 | "headers_to_pass": [ 79 | "*" 80 | ] 81 | } 82 | ], 83 | "name": "计算机设计大赛演示", 84 | "port": "8000" 85 | } -------------------------------------------------------------------------------- /melody-cases/profile/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | profileEngine := gin.Default() 12 | profileEngine.GET("/profile/:id", func(context *gin.Context) { 13 | id := context.Param("id") 14 | context.JSON(http.StatusOK, map[string]interface{}{ 15 | "id": id, 16 | "uuid": "15316368801", 17 | "name": "Grant", 18 | "age": 22, 19 | "role_id": 1, 20 | "vip": false, 21 | "password": "xxxxxxx", 22 | "nick_name": "Grant", 23 | "avatar": "grant.jpg", 24 | "birth": "xxxx-xx-xx", 25 | }) 26 | }) 27 | fmt.Println(profileEngine.Run(":9001")) 28 | } 29 | -------------------------------------------------------------------------------- /melody-cases/role/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | profileEngine := gin.Default() 11 | 12 | profileEngine.GET("/role/:id", func(context *gin.Context) { 13 | roleId := context.Param("id") 14 | context.JSON(http.StatusOK, map[string]interface{}{ 15 | "role_id": roleId, 16 | "role_name": "Administrator", 17 | "role_tag": "Admin", 18 | "authorities": []map[string]interface{}{ 19 | { 20 | "tag": "user add", 21 | "path": "/user/add", 22 | "method": "post", 23 | "active": true, 24 | }, 25 | { 26 | "tag": "user delete", 27 | "path": "/user/delete", 28 | "method": "get", 29 | "active": true, 30 | }, 31 | }, 32 | }) 33 | }) 34 | fmt.Println(profileEngine.Run(":9002")) 35 | } 36 | -------------------------------------------------------------------------------- /middleware/melody-alert/config.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "errors" 5 | "melody/config" 6 | ) 7 | 8 | const ( 9 | namespace = "melody_alert" 10 | ) 11 | 12 | func NewChecker(cfg *config.ServiceConfig) (Checker, error) { 13 | // 解析Service 14 | m, err := parseConfig(cfg.ExtraConfig) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | // 解析Endpoint 20 | for _, endpointConfig := range cfg.Endpoints { 21 | endpointM, err := parseConfig(endpointConfig.ExtraConfig) 22 | if err == nil { 23 | m[endpointConfig.Endpoint] = endpointM 24 | } 25 | } 26 | 27 | checker, err := newChecker(m) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return checker, nil 32 | } 33 | 34 | func parseConfig(extraConfig config.ExtraConfig) (map[string]interface{}, error) { 35 | if _, ok := extraConfig[namespace]; !ok { 36 | return nil, errors.New("no melody_alert") 37 | } 38 | 39 | if fm, ok := extraConfig[namespace].(map[string]interface{}); !ok { 40 | return nil, errors.New("no fields") 41 | } else { 42 | return fm, nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /middleware/melody-alert/model/warning.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Warning struct { 8 | Id int64 `gorm:"id" json:"id"` 9 | Description string `gorm:"description" json:"description"` 10 | TaskName string `gorm:"task_name" json:"task_name"` 11 | CurValue int64 `gorm:"cur_value" json:"cur_value"` 12 | Threshold int64 `gorm:"threshold" json:"threshold"` 13 | Ctime int64 `gorm:"ctime" json:"ctime"` 14 | Handled int `gorm:"handled" json:"handled"` 15 | } 16 | 17 | var ( 18 | Id = new(IdWorker) 19 | WarningList = NewWarningList() 20 | ) 21 | 22 | type Warnings struct { 23 | Warnings []Warning `json:"warnings"` 24 | Lock sync.RWMutex `json:"-"` 25 | WatchChan chan Warning `json:"-"` 26 | } 27 | 28 | func NewWarningList() Warnings { 29 | return Warnings{ 30 | Warnings: make([]Warning, 0), 31 | Lock: sync.RWMutex{}, 32 | WatchChan: make(chan Warning), 33 | } 34 | } 35 | 36 | func (ws *Warnings) Add(warning Warning) { 37 | ws.Lock.Lock() 38 | ws.Warnings = append(ws.Warnings, warning) 39 | ws.Lock.Unlock() 40 | ws.WatchChan <- warning 41 | } 42 | 43 | func (ws *Warnings) ChangeStatus(id int64) { 44 | ws.Lock.Lock() 45 | if ws.Warnings[id-1].Handled == 0 { 46 | ws.Warnings[id-1].Handled = 1 47 | } else { 48 | ws.Warnings[id-1].Handled = 0 49 | } 50 | ws.Lock.Unlock() 51 | } 52 | 53 | type IdWorker struct { 54 | Id int64 55 | Lock sync.RWMutex 56 | } 57 | 58 | func (id *IdWorker) inc() { 59 | id.Lock.Lock() 60 | id.Id++ 61 | id.Lock.Unlock() 62 | } 63 | 64 | func (id *IdWorker) GetId() int64 { 65 | id.inc() 66 | return id.Id 67 | } 68 | -------------------------------------------------------------------------------- /middleware/melody-bloomfilter/bloomfilter.go: -------------------------------------------------------------------------------- 1 | package bloomfilter 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "melody/bf" 8 | rpc_bf "melody/bf/rpc" 9 | "melody/bf/rpc/server" 10 | "melody/config" 11 | "melody/logging" 12 | "net/http" 13 | ) 14 | 15 | const Namespace = "melody_bloomfilter" 16 | 17 | var ( 18 | errNoConfig = errors.New("no config for the bloomfilter") 19 | errWrongConfig = errors.New("invalid config for the bloomfilter") 20 | ) 21 | 22 | type Config struct { 23 | rpc_bf.Config 24 | TokenKeys []string 25 | Headers []string 26 | } 27 | 28 | type Rejecter struct { 29 | BF bf.BloomFilter 30 | TokenKeys []string 31 | Headers []string 32 | } 33 | 34 | // Register registers a bloomfilter given a config and registers the service with consul 35 | func Register(ctx context.Context, serviceName string, cfg config.ServiceConfig, 36 | logger logging.Logger, register func(n string, p int)) (Rejecter, error) { 37 | 38 | data, ok := cfg.ExtraConfig[Namespace] 39 | if !ok { 40 | logger.Debug(errNoConfig.Error()) 41 | return nopRejecter, errNoConfig 42 | } 43 | 44 | raw, err := json.Marshal(data) 45 | if err != nil { 46 | logger.Debug(errWrongConfig.Error()) 47 | return nopRejecter, errWrongConfig 48 | } 49 | 50 | var rpcConfig Config 51 | if err := json.Unmarshal(raw, &rpcConfig); err != nil { 52 | logger.Debug(err.Error(), string(raw)) 53 | return nopRejecter, err 54 | } 55 | 56 | rpcBF := server.New(ctx, rpcConfig.Config) 57 | register(serviceName, rpcConfig.Port) 58 | 59 | return Rejecter{ 60 | BF: rpcBF.Get(), 61 | TokenKeys: rpcConfig.TokenKeys, 62 | Headers: rpcConfig.Headers, 63 | }, nil 64 | } 65 | 66 | func (r *Rejecter) RejectToken(claims map[string]interface{}) bool { 67 | for _, k := range r.TokenKeys { 68 | v, ok := claims[k] 69 | if !ok { 70 | continue 71 | } 72 | data, ok := v.(string) 73 | if !ok { 74 | continue 75 | } 76 | if r.BF.Check([]byte(k + "-" + data)) { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | func (r *Rejecter) RejectHeader(header http.Header) bool { 84 | for _, k := range r.Headers { 85 | data := header.Get(k) 86 | if data == "" { 87 | continue 88 | } 89 | if r.BF.Check([]byte(k + "-" + data)) { 90 | return true 91 | } 92 | } 93 | return false 94 | } 95 | 96 | var nopRejecter = Rejecter{BF: new(bf.EmptySet)} 97 | -------------------------------------------------------------------------------- /middleware/melody-botmonitor/gin/monitor.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "melody/config" 9 | "melody/logging" 10 | botmonitor "melody/middleware/melody-botmonitor" 11 | melody "melody/middleware/melody-botmonitor/melody" 12 | "melody/proxy" 13 | melodygin "melody/router/gin" 14 | ) 15 | 16 | // Register 检查配置,按照需要在gin引擎上注册一个bot检测中间件 17 | func Register(cfg config.ServiceConfig, l logging.Logger, engine *gin.Engine) { 18 | detectorCfg, err := melody.ParseConfig(cfg.ExtraConfig) 19 | if err == melody.ErrNoConfig { 20 | l.Debug("botmonitor middleware: ", err.Error()) 21 | return 22 | } 23 | if err != nil { 24 | l.Warning("botmonitor middleware: ", err.Error()) 25 | return 26 | } 27 | d, err := botmonitor.New(detectorCfg) 28 | if err != nil { 29 | l.Warning("botmonitor middleware: unable to createt the LRU detector:", err.Error()) 30 | return 31 | } 32 | engine.Use(middleware(d)) 33 | } 34 | 35 | // New 检查配置 36 | func New(hf melodygin.HandlerFactory, l logging.Logger) melodygin.HandlerFactory { 37 | return func(cfg *config.EndpointConfig, p proxy.Proxy) gin.HandlerFunc { 38 | next := hf(cfg, p) 39 | 40 | detectorCfg, err := melody.ParseConfig(cfg.ExtraConfig) 41 | if err == melody.ErrNoConfig { 42 | l.Debug("botmonitor: ", err.Error()) 43 | return next 44 | } 45 | if err != nil { 46 | l.Warning("botmonitor: ", err.Error()) 47 | return next 48 | } 49 | 50 | d, err := botmonitor.New(detectorCfg) 51 | if err != nil { 52 | l.Warning("botmonitor: unable to create the LRU detector:", err.Error()) 53 | return next 54 | } 55 | return handler(d, next) 56 | } 57 | } 58 | 59 | func middleware(f botmonitor.DetectorFunc) gin.HandlerFunc { 60 | return func(c *gin.Context) { 61 | if f(c.Request) { 62 | c.AbortWithError(http.StatusForbidden, errBotRejected) 63 | return 64 | } 65 | 66 | c.Next() 67 | } 68 | } 69 | 70 | func handler(f botmonitor.DetectorFunc, next gin.HandlerFunc) gin.HandlerFunc { 71 | return func(c *gin.Context) { 72 | if f(c.Request) { 73 | c.AbortWithError(http.StatusForbidden, errBotRejected) 74 | return 75 | } 76 | 77 | next(c) 78 | } 79 | } 80 | 81 | var errBotRejected = errors.New("bot rejected") 82 | -------------------------------------------------------------------------------- /middleware/melody-botmonitor/gin/monitor_test.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "melody/config" 12 | "melody/logging" 13 | melody "melody/middleware/melody-botmonitor/melody" 14 | "melody/proxy" 15 | melodygin "melody/router/gin" 16 | ) 17 | 18 | func TestRegister(t *testing.T) { 19 | gin.SetMode(gin.TestMode) 20 | 21 | engine := gin.New() 22 | 23 | cfg := config.ServiceConfig{ 24 | ExtraConfig: config.ExtraConfig{ 25 | melody.Namespace: map[string]interface{}{ 26 | "blacklist": []string{"a", "b"}, 27 | "whitelist": []string{"c", "Pingdom.com_bot_version_1.1"}, 28 | "patterns": []string{ 29 | `(Pingdom.com_bot_version_)(\d+)\.(\d+)`, 30 | `(facebookexternalhit)/(\d+)\.(\d+)`, 31 | }, 32 | }, 33 | }, 34 | } 35 | 36 | Register(cfg, logging.NoOp, engine) 37 | 38 | engine.GET("/", func(c *gin.Context) { 39 | c.String(200, "hi!") 40 | }) 41 | 42 | if err := testDetection(engine); err != nil { 43 | t.Error(err) 44 | } 45 | } 46 | 47 | func TestNew(t *testing.T) { 48 | gin.SetMode(gin.TestMode) 49 | 50 | engine := gin.New() 51 | 52 | cfg := &config.EndpointConfig{ 53 | ExtraConfig: config.ExtraConfig{ 54 | melody.Namespace: map[string]interface{}{ 55 | "blacklist": []string{"a", "b"}, 56 | "whitelist": []string{"c", "Pingdom.com_bot_version_1.1"}, 57 | "patterns": []string{ 58 | `(Pingdom.com_bot_version_)(\d+)\.(\d+)`, 59 | `(facebookexternalhit)/(\d+)\.(\d+)`, 60 | }, 61 | }, 62 | }, 63 | } 64 | 65 | proxyfunc := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 66 | return &proxy.Response{IsComplete: true}, nil 67 | } 68 | 69 | engine.GET("/", New(melodygin.EndpointHandler, logging.NoOp)(cfg, proxyfunc)) 70 | 71 | if err := testDetection(engine); err != nil { 72 | t.Error(err) 73 | } 74 | } 75 | 76 | func testDetection(engine *gin.Engine) error { 77 | for i, ua := range []string{ 78 | "abcd", 79 | "", 80 | "c", 81 | "Pingdom.com_bot_version_1.1", 82 | } { 83 | req, _ := http.NewRequest("GET", "http://example.com", nil) 84 | req.Header.Add("User-Agent", ua) 85 | 86 | w := httptest.NewRecorder() 87 | engine.ServeHTTP(w, req) 88 | 89 | if w.Result().StatusCode != 200 { 90 | return fmt.Errorf("the req #%d has been detected as a bot: %s", i, ua) 91 | } 92 | } 93 | 94 | for i, ua := range []string{ 95 | "a", 96 | "b", 97 | "facebookexternalhit/1.1", 98 | "Pingdom.com_bot_version_1.2", 99 | } { 100 | req, _ := http.NewRequest("GET", "http://example.com", nil) 101 | req.Header.Add("User-Agent", ua) 102 | 103 | w := httptest.NewRecorder() 104 | engine.ServeHTTP(w, req) 105 | 106 | if w.Result().StatusCode != http.StatusForbidden { 107 | return fmt.Errorf("the req #%d has not been detected as a bot: %s", i, ua) 108 | } 109 | } 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /middleware/melody-botmonitor/melody/config.go: -------------------------------------------------------------------------------- 1 | package melody 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "melody/config" 8 | botmonitor "melody/middleware/melody-botmonitor" 9 | ) 10 | 11 | // Namespace 命名空间 12 | const Namespace = "melody_botmonitor" 13 | 14 | // ErrNoConfig 当没有为模块定义配置时,将返回ErrNoConfig 15 | var ErrNoConfig = errors.New("no config defined for the module") 16 | 17 | // ParseConfig 从ExtraConfig 中提取模块配置,并返回一个适合使用botmonitor包的结构 18 | func ParseConfig(cfg config.ExtraConfig) (botmonitor.Config, error) { 19 | res := botmonitor.Config{} 20 | e, ok := cfg[Namespace] 21 | if !ok { 22 | return res, ErrNoConfig 23 | } 24 | b, err := json.Marshal(e) 25 | if err != nil { 26 | return res, err 27 | } 28 | err = json.Unmarshal(b, &res) 29 | return res, err 30 | } 31 | -------------------------------------------------------------------------------- /middleware/melody-botmonitor/monitor.go: -------------------------------------------------------------------------------- 1 | package botmonitor 2 | 3 | import ( 4 | "net/http" 5 | "regexp" 6 | 7 | lru "github.com/hashicorp/golang-lru" 8 | ) 9 | 10 | // Config 配置定义了检测器的行为 11 | type Config struct { 12 | Blacklist []string 13 | Whitelist []string 14 | Patterns []string 15 | CacheSize int 16 | } 17 | 18 | // DetectorFunc 是一个func,如果一个请求是由一个机器人发出的,它就会进行chek 19 | type DetectorFunc func(r *http.Request) bool 20 | 21 | // New 根据参数返回带有或不带LRU缓存的检测器函数 22 | func New(cfg Config) (DetectorFunc, error) { 23 | if cfg.CacheSize == 0 { 24 | d, err := NewDetector(cfg) 25 | return d.IsBot, err 26 | } 27 | 28 | d, err := NewLRU(cfg) 29 | return d.IsBot, err 30 | } 31 | 32 | // NewDetector 创建一个检测器 33 | func NewDetector(cfg Config) (*Detector, error) { 34 | blacklist := make(map[string]struct{}, len(cfg.Blacklist)) 35 | for _, e := range cfg.Blacklist { 36 | blacklist[e] = struct{}{} 37 | } 38 | whitelist := make(map[string]struct{}, len(cfg.Whitelist)) 39 | for _, e := range cfg.Whitelist { 40 | whitelist[e] = struct{}{} 41 | } 42 | patterns := make([]*regexp.Regexp, len(cfg.Patterns)) 43 | for i, p := range cfg.Patterns { 44 | rp, err := regexp.Compile(p) 45 | if err != nil { 46 | return nil, err 47 | } 48 | patterns[i] = rp 49 | } 50 | return &Detector{ 51 | blacklist: blacklist, 52 | whitelist: whitelist, 53 | patterns: patterns, 54 | }, nil 55 | } 56 | 57 | // Detector (检测器)是一种能够检测机器人发出的请求的结构体 58 | type Detector struct { 59 | blacklist map[string]struct{} 60 | whitelist map[string]struct{} 61 | patterns []*regexp.Regexp 62 | } 63 | 64 | // IsBot : 如果请求是由机器人发出的,则IsBot返回true 65 | func (d *Detector) IsBot(r *http.Request) bool { 66 | userAgent := r.Header.Get("User-Agent") 67 | if userAgent == "" { 68 | return false 69 | } 70 | if _, ok := d.whitelist[userAgent]; ok { 71 | return false 72 | } 73 | if _, ok := d.blacklist[userAgent]; ok { 74 | return true 75 | } 76 | for _, p := range d.patterns { 77 | if p.MatchString(userAgent) { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | 84 | // NewLRU 创建一个新的LRUDetector 85 | func NewLRU(cfg Config) (*LRUDetector, error) { 86 | d, err := NewDetector(cfg) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | cache, err := lru.New(cfg.CacheSize) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return &LRUDetector{ 97 | detectorFunc: d.IsBot, 98 | cache: cache, 99 | }, nil 100 | } 101 | 102 | // LRUDetector 是一种能够检测bot发出的请求并缓存结果以供将来重用的结构 103 | type LRUDetector struct { 104 | detectorFunc DetectorFunc 105 | cache *lru.Cache 106 | } 107 | 108 | // IsBot 如果请求是由机器人发出的,则IsBot返回true 109 | func (d *LRUDetector) IsBot(r *http.Request) bool { 110 | userAgent := r.Header.Get("User-Agent") 111 | cached, ok := d.cache.Get(userAgent) 112 | if ok { 113 | return cached.(bool) 114 | } 115 | 116 | res := d.detectorFunc(r) 117 | d.cache.Add(userAgent, res) 118 | 119 | return res 120 | } 121 | -------------------------------------------------------------------------------- /middleware/melody-botmonitor/monitor_bechmark_test.go: -------------------------------------------------------------------------------- 1 | package botmonitor 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkDetector(b *testing.B) { 9 | d, err := New(Config{ 10 | Blacklist: []string{"a", "b"}, 11 | Whitelist: []string{"c", "Pingdom.com_bot_version_1.1"}, 12 | Patterns: []string{ 13 | `(Pingdom.com_bot_version_)(\d+)\.(\d+)`, 14 | `(facebookexternalhit)/(\d+)\.(\d+)`, 15 | }, 16 | }) 17 | if err != nil { 18 | b.Error(err) 19 | return 20 | } 21 | 22 | becnhDetection(b, d) 23 | } 24 | 25 | func BenchmarkLRUDetector(b *testing.B) { 26 | d, err := New(Config{ 27 | Blacklist: []string{"a", "b"}, 28 | Whitelist: []string{"c", "Pingdom.com_bot_version_1.1"}, 29 | Patterns: []string{ 30 | `(Pingdom.com_bot_version_)(\d+)\.(\d+)`, 31 | `(facebookexternalhit)/(\d+)\.(\d+)`, 32 | }, 33 | CacheSize: 10000, 34 | }) 35 | if err != nil { 36 | b.Error(err) 37 | return 38 | } 39 | 40 | becnhDetection(b, d) 41 | } 42 | 43 | func becnhDetection(b *testing.B, f DetectorFunc) { 44 | for _, tc := range []struct { 45 | name string 46 | ua string 47 | }{ 48 | {"ok_1", "abcd"}, 49 | {"ok_2", ""}, 50 | {"ok_3", "c"}, 51 | {"ok_4", "Pingdom.com_bot_version_1.1"}, 52 | {"ko_1", "a"}, 53 | {"ko_2", "b"}, 54 | {"ko_3", "facebookexternalhit/1.1"}, 55 | {"ko_4", "Pingdom.com_bot_version_1.2"}, 56 | } { 57 | 58 | req, _ := http.NewRequest("GET", "http://example.com", nil) 59 | req.Header.Add("User-Agent", tc.ua) 60 | b.Run(tc.name, func(b *testing.B) { 61 | for i := 0; i < b.N; i++ { 62 | f(req) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /middleware/melody-botmonitor/monitor_test.go: -------------------------------------------------------------------------------- 1 | package botmonitor 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestNew_noLRU(t *testing.T) { 10 | d, err := New(Config{ 11 | Blacklist: []string{"a", "b"}, 12 | Whitelist: []string{"c", "Pingdom.com_bot_version_1.1"}, 13 | Patterns: []string{ 14 | `(Pingdom.com_bot_version_)(\d+)\.(\d+)`, 15 | `(facebookexternalhit)/(\d+)\.(\d+)`, 16 | }, 17 | }) 18 | if err != nil { 19 | t.Error(err) 20 | return 21 | } 22 | 23 | if err := testDetection(d); err != nil { 24 | t.Error(err) 25 | } 26 | } 27 | 28 | func TestNew_LRU(t *testing.T) { 29 | d, err := New(Config{ 30 | Blacklist: []string{"a", "b"}, 31 | Whitelist: []string{"c", "Pingdom.com_bot_version_1.1"}, 32 | Patterns: []string{ 33 | `(Pingdom.com_bot_version_)(\d+)\.(\d+)`, 34 | `(facebookexternalhit)/(\d+)\.(\d+)`, 35 | }, 36 | CacheSize: 10000, 37 | }) 38 | if err != nil { 39 | t.Error(err) 40 | return 41 | } 42 | 43 | if err := testDetection(d); err != nil { 44 | t.Error(err) 45 | } 46 | } 47 | 48 | func testDetection(f DetectorFunc) error { 49 | for i, ua := range []string{ 50 | "abcd", 51 | "", 52 | "c", 53 | "Pingdom.com_bot_version_1.1", 54 | } { 55 | req, _ := http.NewRequest("GET", "http://example.com", nil) 56 | req.Header.Add("User-Agent", ua) 57 | if f(req) { 58 | return fmt.Errorf("the req #%d has been detected as a bot: %s", i, ua) 59 | } 60 | } 61 | 62 | for i, ua := range []string{ 63 | "a", 64 | "b", 65 | "facebookexternalhit/1.1", 66 | "Pingdom.com_bot_version_1.2", 67 | } { 68 | req, _ := http.NewRequest("GET", "http://example.com", nil) 69 | req.Header.Add("User-Agent", ua) 70 | if !f(req) { 71 | return fmt.Errorf("the req #%d has not been detected as a bot: %s", i, ua) 72 | } 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /middleware/melody-circuitbreaker/gobreaker.go: -------------------------------------------------------------------------------- 1 | package gobreaker 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sony/gobreaker" 6 | "melody/config" 7 | "melody/logging" 8 | "time" 9 | ) 10 | 11 | const Namespace = "melody_circuitbreaker" 12 | 13 | type Config struct { 14 | //给定的时间间隔(秒) 15 | Interval int 16 | //等待时间窗口(秒) 17 | Timeout int 18 | //连续故障数 19 | MaxErrors int 20 | //断路器状态发生改变时,是否log 21 | LogStatusChange bool 22 | } 23 | 24 | // 空实现 25 | var DefaultCfg = Config{} 26 | 27 | // 获取断路器配置 28 | func ConfigGetter(e config.ExtraConfig) interface{} { 29 | v, ok := e[Namespace] 30 | if !ok { 31 | return DefaultCfg 32 | } 33 | temp, ok := v.(map[string]interface{}) 34 | if !ok { 35 | return DefaultCfg 36 | } 37 | cfg := Config{} 38 | if i, ok := temp["interval"]; ok { 39 | switch in := i.(type) { 40 | case int: 41 | cfg.Interval = in 42 | case int64: 43 | cfg.Interval = int(in) 44 | } 45 | } 46 | if v, ok := temp["timeout"]; ok { 47 | switch i := v.(type) { 48 | case int: 49 | cfg.Timeout = i 50 | case float64: 51 | cfg.Timeout = int(i) 52 | } 53 | } 54 | if v, ok := temp["maxErrors"]; ok { 55 | switch i := v.(type) { 56 | case int: 57 | cfg.MaxErrors = i 58 | case float64: 59 | cfg.MaxErrors = int(i) 60 | } 61 | } 62 | value, ok := temp["logStatusChange"].(bool) 63 | cfg.LogStatusChange = ok && value 64 | 65 | return cfg 66 | } 67 | 68 | // 基于sony的断路器,包装断路器对象 69 | func NewCircuitBreaker(config Config, logger logging.Logger) *gobreaker.CircuitBreaker { 70 | settings := gobreaker.Settings{ 71 | Name: "Melody CircuitBreaker", 72 | Interval: time.Duration(config.Interval) * time.Second, 73 | Timeout: time.Duration(config.Timeout) * time.Second, 74 | ReadyToTrip: func(counts gobreaker.Counts) bool { 75 | return counts.ConsecutiveFailures > uint32(config.MaxErrors) 76 | }, 77 | } 78 | 79 | if config.LogStatusChange { 80 | settings.OnStateChange = func(name string, from gobreaker.State, to gobreaker.State) { 81 | logger.Warning(fmt.Sprintf("circuit breaker named '%s' went from '%s' to '%s'", name, from.String(), to.String())) 82 | } 83 | } 84 | 85 | return gobreaker.NewCircuitBreaker(settings) 86 | } 87 | -------------------------------------------------------------------------------- /middleware/melody-circuitbreaker/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "melody/config" 6 | "melody/logging" 7 | gobreaker "melody/middleware/melody-circuitbreaker" 8 | "melody/proxy" 9 | ) 10 | 11 | func BackendFactory(next proxy.BackendFactory, logger logging.Logger) proxy.BackendFactory { 12 | return func(backend *config.Backend) proxy.Proxy { 13 | return NewMiddleware(backend, logger)(next(backend)) 14 | } 15 | } 16 | 17 | func NewMiddleware(remote *config.Backend, logger logging.Logger) proxy.Middleware { 18 | config := gobreaker.ConfigGetter(remote.ExtraConfig).(gobreaker.Config) 19 | if config == gobreaker.DefaultCfg { 20 | return proxy.EmptyMiddleware 21 | } 22 | 23 | breaker := gobreaker.NewCircuitBreaker(config, logger) 24 | 25 | return func(next ...proxy.Proxy) proxy.Proxy { 26 | if len(next) > 1 { 27 | panic(proxy.ErrTooManyProxies) 28 | } 29 | 30 | return func(ctx context.Context, request *proxy.Request) (response *proxy.Response, err error) { 31 | res, err := breaker.Execute(func() (i interface{}, err error) { 32 | return next[0](ctx, request) 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return res.(*proxy.Response), err 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /middleware/melody-consul/client.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "context" 5 | "github.com/go-contrib/uuid" 6 | "github.com/hashicorp/consul/api" 7 | "melody/config" 8 | "melody/logging" 9 | ) 10 | 11 | func Register(ctx context.Context, e config.ExtraConfig, port int, serviceName string, logger logging.Logger) error { 12 | cfg, err := parse(e, port) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | cfg.Name = serviceName 18 | 19 | return register(ctx, cfg, logger) 20 | } 21 | 22 | func register(ctx context.Context, cfg Config, logger logging.Logger) error { 23 | consulConfig := api.DefaultConfig() 24 | consulConfig.Address = cfg.Address 25 | c, err := api.NewClient(consulConfig) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | service := &api.AgentServiceRegistration{ 31 | Name: cfg.Name, 32 | Port: cfg.Port, 33 | Tags: cfg.Tags, 34 | ID: uuid.NewV1().String(), 35 | } 36 | 37 | if err := c.Agent().ServiceRegister(service); err != nil { 38 | return err 39 | } 40 | 41 | go func() { 42 | <-ctx.Done() 43 | 44 | if err := c.Agent().ServiceDeregister(service.ID); err != nil { 45 | logger.Info("error when trying to deregister service", service.ID, ":", err) 46 | } 47 | }() 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /middleware/melody-consul/config.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "fmt" 5 | "melody/config" 6 | ) 7 | 8 | type Config struct { 9 | Address, Name string 10 | Port int 11 | Tags []string 12 | } 13 | 14 | // Namespace is the key to use to store and access the custom config data 15 | const Namespace = "melody_consul" 16 | 17 | var ( 18 | // ErrNoConfig is the error to be returned when there is no config with the consul namespace 19 | ErrNoConfig = fmt.Errorf("unable to create the consul client: no config") 20 | // ErrBadConfig is the error to be returned when the config is not well defined 21 | ErrBadConfig = fmt.Errorf("unable to create the consul client with the received config") 22 | // ErrNoMachines is the error to be returned when the config has not defined one or more servers 23 | ErrNoMachines = fmt.Errorf("unable to create the consul client without a set of servers") 24 | ) 25 | 26 | func parse(e config.ExtraConfig, port int) (Config, error) { 27 | cfg := Config{ 28 | Name: "melody", 29 | Port: port, 30 | } 31 | v, ok := e[Namespace] 32 | if !ok { 33 | return cfg, ErrNoConfig 34 | } 35 | tmp, ok := v.(map[string]interface{}) 36 | if !ok { 37 | return cfg, ErrBadConfig 38 | } 39 | a, ok := tmp["address"] 40 | if !ok { 41 | return cfg, ErrNoMachines 42 | } 43 | cfg.Address, ok = a.(string) 44 | 45 | if !ok { 46 | return cfg, ErrNoMachines 47 | } 48 | 49 | if a, ok = tmp["name"]; ok { 50 | cfg.Name, ok = a.(string) 51 | } 52 | 53 | cfg.Tags = parseTags(tmp) 54 | 55 | return cfg, nil 56 | } 57 | 58 | func parseTags(cfg map[string]interface{}) []string { 59 | result := []string{} 60 | tags, ok := cfg["tags"] 61 | if !ok { 62 | return result 63 | } 64 | tgs, ok := tags.([]interface{}) 65 | if !ok { 66 | return result 67 | } 68 | for _, tg := range tgs { 69 | if t, ok := tg.(string); ok { 70 | result = append(result, t) 71 | } 72 | } 73 | 74 | return result 75 | } 76 | -------------------------------------------------------------------------------- /middleware/melody-cors/cors.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "melody/config" 5 | "time" 6 | ) 7 | 8 | // Namespace the key of corss domain 9 | const Namespace = "melody_cors" 10 | 11 | // Config holds the configuration of CORS 12 | type Config struct { 13 | AllowOrigins []string 14 | AllowMethods []string 15 | AllowHeaders []string 16 | ExposeHeaders []string 17 | AllowCredentials bool 18 | MaxAge time.Duration 19 | } 20 | 21 | // GetConfig 获取cors配置strcut 22 | func GetConfig(e config.ExtraConfig) interface{} { 23 | v, ok := e[Namespace] 24 | if !ok { 25 | return nil 26 | } 27 | 28 | temp, ok := v.(map[string]interface{}) 29 | if !ok { 30 | return nil 31 | } 32 | 33 | cfg := Config{} 34 | cfg.AllowOrigins = getList(temp, "allow_origins") 35 | cfg.AllowMethods = getList(temp, "allow_methods") 36 | cfg.AllowHeaders = getList(temp, "allow_headers") 37 | cfg.ExposeHeaders = getList(temp, "expose_headers") 38 | if cr, ok := temp["allow_credentials"]; ok { 39 | if cre, ok := cr.(bool); ok { 40 | cfg.AllowCredentials = cre 41 | } 42 | } 43 | 44 | if a, ok := temp["max_age"]; ok { 45 | if d, err := time.ParseDuration(a.(string)); err == nil { 46 | cfg.MaxAge = d 47 | } 48 | } 49 | 50 | return cfg 51 | } 52 | 53 | // getList return array data from map via key 54 | func getList(data map[string]interface{}, name string) []string { 55 | out := []string{} 56 | if vs, ok := data[name]; ok { 57 | if v, ok := vs.([]interface{}); ok { 58 | for _, s := range v { 59 | if j, ok := s.(string); ok { 60 | out = append(out, j) 61 | } 62 | } 63 | } 64 | } 65 | return out 66 | } 67 | -------------------------------------------------------------------------------- /middleware/melody-cors/cors_test.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestConfigGetter(t *testing.T) { 10 | sampleCfg := map[string]interface{}{} 11 | serialized := []byte(`{ "melody_cors": { 12 | "allow_origins": [ "http://localhost", "http://www.example.com" ], 13 | "allow_headers": [ "X-Test", "Content-Type"], 14 | "allow_methods": [ "POST", "GET" ], 15 | "expose_headers": [ "Content-Type" ], 16 | "allow_credentials": false, 17 | "max_age": "24h" 18 | } 19 | }`) 20 | json.Unmarshal(serialized, &sampleCfg) 21 | testCfg := GetConfig(sampleCfg).(Config) 22 | 23 | if len(testCfg.AllowOrigins) != 2 { 24 | t.Error("Should have exactly 2 allowed origins.\n") 25 | } 26 | for i, v := range []string{"http://localhost", "http://www.example.com"} { 27 | if testCfg.AllowOrigins[i] != v { 28 | t.Errorf("Invalid value %s should be %s\n", testCfg.AllowOrigins[i], v) 29 | } 30 | } 31 | if len(testCfg.AllowHeaders) != 2 { 32 | t.Error("Should have exactly 2 allowed headers.\n") 33 | } 34 | for i, v := range []string{"X-Test", "Content-Type"} { 35 | if testCfg.AllowHeaders[i] != v { 36 | t.Errorf("Invalid value %s should be %s\n", testCfg.AllowHeaders[i], v) 37 | } 38 | } 39 | if len(testCfg.AllowMethods) != 2 { 40 | t.Error("Should have exactly 2 allowed headers.\n") 41 | } 42 | for i, v := range []string{"POST", "GET"} { 43 | if testCfg.AllowMethods[i] != v { 44 | t.Errorf("Invalid value %s should be %s\n", testCfg.AllowMethods[i], v) 45 | } 46 | } 47 | if len(testCfg.ExposeHeaders) != 1 { 48 | t.Error("Should have exactly 2 allowed headers.\n") 49 | } 50 | for i, v := range []string{"Content-Type"} { 51 | if testCfg.ExposeHeaders[i] != v { 52 | t.Errorf("Invalid value %s should be %s\n", testCfg.ExposeHeaders[i], v) 53 | } 54 | } 55 | if testCfg.AllowCredentials { 56 | t.Error("Allow Credentials should be disabled.\n") 57 | } 58 | 59 | if testCfg.MaxAge != 24*time.Hour { 60 | t.Errorf("Unexpected collection time: %v\n", testCfg.MaxAge) 61 | } 62 | } 63 | 64 | func TestDefaultConfiguration(t *testing.T) { 65 | sampleCfg := map[string]interface{}{} 66 | serialized := []byte(`{ "melody_cors": { 67 | "allow_origins": [ "http://www.example.com" ] 68 | }}`) 69 | json.Unmarshal(serialized, &sampleCfg) 70 | defaultCfg := GetConfig(sampleCfg).(Config) 71 | if defaultCfg.AllowOrigins[0] != "http://www.example.com" { 72 | t.Error("Wrong AllowOrigin.\n") 73 | } 74 | } 75 | 76 | func TestWrongConfiguration(t *testing.T) { 77 | sampleCfg := map[string]interface{}{} 78 | if _, ok := GetConfig(sampleCfg).(Config); ok { 79 | t.Error("The config should be nil\n") 80 | } 81 | badCfg := map[string]interface{}{Namespace: "test"} 82 | if _, ok := GetConfig(badCfg).(Config); ok { 83 | t.Error("The config should be nil\n") 84 | } 85 | } 86 | 87 | func TestEmptyConfiguration(t *testing.T) { 88 | noOriginCfg := map[string]interface{}{} 89 | serialized := []byte(`{ "melody_cors": { 90 | } 91 | }`) 92 | json.Unmarshal(serialized, &noOriginCfg) 93 | if v, ok := GetConfig(noOriginCfg).(Config); !ok { 94 | t.Errorf("The configuration should not be empty: %v\n", v) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /middleware/melody-cors/gin/cors.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "melody/config" 5 | 6 | gincors "melody/middleware/melody-cors" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/rs/cors" 10 | wrapper "github.com/rs/cors/wrapper/gin" 11 | ) 12 | 13 | // New 创建cors middleware 14 | func New(extra config.ExtraConfig) gin.HandlerFunc { 15 | c := gincors.GetConfig(extra) 16 | if c == nil { 17 | return nil 18 | } 19 | 20 | t, ok := c.(gincors.Config) 21 | if !ok { 22 | return nil 23 | } 24 | 25 | return wrapper.New( 26 | cors.Options{ 27 | AllowedOrigins: t.AllowOrigins, 28 | AllowedMethods: t.AllowMethods, 29 | AllowedHeaders: t.AllowHeaders, 30 | ExposedHeaders: t.ExposeHeaders, 31 | AllowCredentials: t.AllowCredentials, 32 | MaxAge: int(t.MaxAge.Seconds()), 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /middleware/melody-etcd/config.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "melody/config" 7 | "time" 8 | ) 9 | 10 | // Namespace is the key to use to store and access the custom config data 11 | const Namespace = "melody_etcd" 12 | 13 | var ( 14 | // ErrNoConfig is the error to be returned when there is no config with the etcd namespace 15 | ErrNoConfig = fmt.Errorf("unable to create the etcd client: no config") 16 | // ErrBadConfig is the error to be returned when the config is not well defined 17 | ErrBadConfig = fmt.Errorf("unable to create the etcd client with the received config") 18 | // ErrNoMachines is the error to be returned when the config has not defined one or more servers 19 | ErrNoMachines = fmt.Errorf("unable to create the etcd client without a set of servers") 20 | ) 21 | 22 | // New creates an etcd client with the config extracted from the extra config param 23 | func New(ctx context.Context, e config.ExtraConfig) (Client, error) { 24 | v, ok := e[Namespace] 25 | if !ok { 26 | return nil, ErrNoConfig 27 | } 28 | tmp, ok := v.(map[string]interface{}) 29 | if !ok { 30 | return nil, ErrBadConfig 31 | } 32 | machines, err := parseMachines(tmp) 33 | if err != nil { 34 | return nil, err 35 | } 36 | etcdClient, err := NewClient(ctx, machines, parseOptions(tmp)) 37 | 38 | return etcdClient, err 39 | } 40 | 41 | func parseMachines(cfg map[string]interface{}) ([]string, error) { 42 | result := []string{} 43 | machines, ok := cfg["machines"] 44 | if !ok { 45 | return result, ErrNoMachines 46 | } 47 | ms, ok := machines.([]interface{}) 48 | if !ok { 49 | return result, ErrNoMachines 50 | } 51 | for _, m := range ms { 52 | if machine, ok := m.(string); ok { 53 | result = append(result, machine) 54 | } 55 | } 56 | if len(result) == 0 { 57 | return result, ErrNoMachines 58 | } 59 | return result, nil 60 | } 61 | 62 | func parseOptions(cfg map[string]interface{}) ClientOptions { 63 | options := ClientOptions{} 64 | v, ok := cfg["options"] 65 | if !ok { 66 | return options 67 | } 68 | tmp := v.(map[string]interface{}) 69 | 70 | if o, ok := tmp["cert"]; ok { 71 | options.Cert = o.(string) 72 | } 73 | 74 | if o, ok := tmp["key"]; ok { 75 | options.Key = o.(string) 76 | } 77 | 78 | if o, ok := tmp["cacert"]; ok { 79 | options.CACert = o.(string) 80 | } 81 | 82 | if o, ok := tmp["dial_timeout"]; ok { 83 | if d, err := parseDuration(o); err == nil { 84 | options.DialTimeout = d 85 | } 86 | } 87 | 88 | if o, ok := tmp["dial_keepalive"]; ok { 89 | if d, err := parseDuration(o); err == nil { 90 | options.DialKeepAlive = d 91 | } 92 | } 93 | 94 | return options 95 | } 96 | 97 | func parseDuration(v interface{}) (time.Duration, error) { 98 | s, ok := v.(string) 99 | if !ok { 100 | return 0, fmt.Errorf("unable to parse %v as a time.Duration\n", v) 101 | } 102 | return time.ParseDuration(s) 103 | } 104 | -------------------------------------------------------------------------------- /middleware/melody-etcd/subscriber.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "melody/config" 7 | "melody/sd" 8 | "sync" 9 | ) 10 | 11 | var ( 12 | subscribers = map[string]sd.Subscriber{} 13 | subscribersMutex = &sync.Mutex{} 14 | fallbackSubscriberFactory = sd.FixedSubscriberFactory 15 | NoClientError = errors.New("errors: no SD client") 16 | ) 17 | 18 | // SubscriberFactory builds a an etcd subscriber SubscriberFactory with the received etcd client 19 | func SubscriberFactory(ctx context.Context, c Client) sd.SubscriberFactory { 20 | return func(cfg *config.Backend) sd.Subscriber { 21 | if len(cfg.Host) == 0 { 22 | return fallbackSubscriberFactory(cfg) 23 | } 24 | subscribersMutex.Lock() 25 | defer subscribersMutex.Unlock() 26 | // 为什么只拿 backend 中的第一个 host 27 | if sf, ok := subscribers[cfg.Host[0]]; ok { 28 | return sf 29 | } 30 | sf, err := NewSubscriber(ctx, c, cfg.Host[0]) 31 | if err != nil { 32 | return fallbackSubscriberFactory(cfg) 33 | } 34 | subscribers[cfg.Host[0]] = sf 35 | return sf 36 | } 37 | } 38 | 39 | // Code taken from https://github.com/go-kit/kit/blob/master/sd/etcd/instancer.go 40 | 41 | // Subscriber keeps instances stored in a certain etcd keyspace cached in a fixed subscriber. Any kind of 42 | // change in that keyspace is watched and will update the Subscriber's list of hosts. 43 | type Subscriber struct { 44 | cache *sd.FixedSubscriber 45 | mutex *sync.RWMutex 46 | client Client 47 | prefix string 48 | ctx context.Context 49 | } 50 | 51 | // NewSubscriber returns an etcd subscriber. It will start watching the given 52 | // prefix for changes, and update the subscribers. 53 | func NewSubscriber(ctx context.Context, c Client, prefix string) (*Subscriber, error) { 54 | s := &Subscriber{ 55 | client: c, 56 | prefix: prefix, 57 | cache: &sd.FixedSubscriber{}, 58 | ctx: ctx, 59 | mutex: &sync.RWMutex{}, 60 | } 61 | 62 | if s.client == nil { 63 | return nil, NoClientError 64 | } 65 | 66 | // 获取 key 为 s.prefix 的所有value 67 | instances, err := s.client.GetEntries(s.prefix) 68 | if err != nil { 69 | return nil, err 70 | } 71 | // 更新缓存中的 value 72 | *(s.cache) = sd.FixedSubscriber(instances) 73 | 74 | go s.loop() 75 | 76 | return s, nil 77 | } 78 | 79 | // Hosts implements the subscriber interface 80 | func (s Subscriber) Hosts() ([]string, error) { 81 | s.mutex.RLock() 82 | defer s.mutex.RUnlock() 83 | return s.cache.Hosts() 84 | } 85 | 86 | func (s *Subscriber) loop() { 87 | ch := make(chan struct{}) 88 | // 监听 key 为 s.prefix 的 value,一旦 value 改变,则给 ch 这个 chan 发送消息,引起缓存更新 89 | go s.client.WatchPrefix(s.prefix, ch) 90 | for { 91 | select { 92 | case <-ch: 93 | instances, err := s.client.GetEntries(s.prefix) 94 | if err != nil { 95 | continue 96 | } 97 | s.mutex.Lock() 98 | *(s.cache) = sd.FixedSubscriber(instances) 99 | s.mutex.Unlock() 100 | 101 | case <-s.ctx.Done(): 102 | return 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /middleware/melody-gelf/log.go: -------------------------------------------------------------------------------- 1 | package gelf 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "gopkg.in/Graylog2/go-gelf.v2/gelf" 7 | "io" 8 | "melody/config" 9 | ) 10 | 11 | const Namespace = "melody_gelf" 12 | 13 | var ( 14 | tcpWriter = gelf.NewTCPWriter 15 | udpWriter = gelf.NewUDPWriter 16 | ErrorConfig = fmt.Errorf("not found extra config about melody-gelf module") 17 | ErrorMissAddress = errors.New("miss gelf address to send log") 18 | ) 19 | 20 | type Config struct { 21 | Addr string 22 | TCPEnable bool 23 | } 24 | 25 | func NewWriter(cfg config.ExtraConfig) (io.Writer, error) { 26 | g, ok := GetConfig(cfg).(Config) 27 | if !ok { 28 | return nil, ErrorConfig 29 | } 30 | 31 | if g.Addr == "" { 32 | return nil, ErrorMissAddress 33 | } 34 | 35 | if g.TCPEnable { 36 | return tcpWriter(g.Addr) 37 | } 38 | 39 | return udpWriter(g.Addr) 40 | } 41 | 42 | func GetConfig(cfg config.ExtraConfig) interface{} { 43 | v, ok := cfg[Namespace] 44 | 45 | if !ok { 46 | return nil 47 | } 48 | 49 | t, ok := v.(map[string]interface{}) 50 | if !ok { 51 | return nil 52 | } 53 | 54 | addr, ok := t["addr"] 55 | if !ok { 56 | return nil 57 | } 58 | 59 | enable, ok := t["tcp_enable"] 60 | if !ok { 61 | return nil 62 | } 63 | 64 | return Config{ 65 | Addr: addr.(string), 66 | TCPEnable: enable.(bool), 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /middleware/melody-httpsecure/gin/httpsecure.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "errors" 5 | "melody/config" 6 | httpsecure "melody/middleware/melody-httpsecure" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/unrolled/secure" 10 | ) 11 | 12 | var ( 13 | errorNoSecureConfig = errors.New("no extra config for http secure middleware") 14 | ) 15 | 16 | // Register http security middleware in engine 17 | func Register(c config.ExtraConfig, engine *gin.Engine) error { 18 | v, ok := httpsecure.GetConfig(c).(secure.Options) 19 | if !ok { 20 | return errorNoSecureConfig 21 | } 22 | 23 | engine.Use(secureMw(v)) 24 | 25 | return nil 26 | } 27 | 28 | func secureMw(o secure.Options) gin.HandlerFunc { 29 | secureMiddle := secure.New(o) 30 | return func(c *gin.Context) { 31 | err := secureMiddle.Process(c.Writer, c.Request) 32 | if err != nil { 33 | c.Abort() 34 | return 35 | } 36 | 37 | if status := c.Writer.Status(); status > 300 && status < 399 { 38 | c.Abort() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /middleware/melody-httpsecure/gin/httpsecure_test.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "melody/config" 5 | httpsecure "melody/middleware/melody-httpsecure" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func TestRegister(t *testing.T) { 14 | gin.SetMode(gin.TestMode) 15 | engine := gin.New() 16 | cfg := config.ExtraConfig{ 17 | httpsecure.Namespace: map[string]interface{}{ 18 | "allowed_hosts": []interface{}{"host1", "subdomain1.host2", "subdomain2.host2"}, 19 | }, 20 | } 21 | if err := Register(cfg, engine); err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | engine.GET("/should_access", func(c *gin.Context) { 26 | c.JSON(200, gin.H{"status": "ok"}) 27 | }) 28 | engine.GET("/never_access", func(c *gin.Context) { 29 | t.Error("unexpected request!", c.Request.URL.String()) 30 | c.JSON(418, gin.H{"status": "ko"}) 31 | }) 32 | engine.GET("/no_headers", func(c *gin.Context) { 33 | c.Redirect(http.StatusPermanentRedirect, "/target") 34 | }) 35 | 36 | for status, URLs := range map[int][]string{ 37 | http.StatusOK: { 38 | "http://host1/should_access", 39 | "https://host1/should_access", 40 | "http://subdomain1.host2/should_access", 41 | "https://subdomain2.host2/should_access", 42 | }, 43 | http.StatusInternalServerError: { 44 | "http://unknown/never_access", 45 | "https://subdomain.host1/never_access", 46 | "http://host2/never_access", 47 | "https://subdomain3.host2/never_access", 48 | }, 49 | } { 50 | for _, URL := range URLs { 51 | w := httptest.NewRecorder() 52 | req, _ := http.NewRequest("GET", URL, nil) 53 | engine.ServeHTTP(w, req) 54 | if w.Result().StatusCode != status { 55 | t.Errorf("request %s unexpected status code! want %d, have %d\n", URL, status, w.Result().StatusCode) 56 | } 57 | } 58 | } 59 | 60 | URL := "https://host1/no_headers" 61 | w := httptest.NewRecorder() 62 | req, _ := http.NewRequest("GET", URL, nil) 63 | engine.ServeHTTP(w, req) 64 | if w.Result().StatusCode != http.StatusPermanentRedirect { 65 | t.Errorf("request %s unexpected status code! want %d, have %d\n", URL, http.StatusPermanentRedirect, w.Result().StatusCode) 66 | } 67 | if w.Result().Header.Get("Location") != "/target" { 68 | t.Error("unexpected value for the location header:", w.Result().Header.Get("Location")) 69 | } 70 | if len(w.Result().Header) != 2 { 71 | t.Error("unexpected number of headers:", len(w.Result().Header), w.Result().Header) 72 | } 73 | } 74 | 75 | func TestRegister_ko(t *testing.T) { 76 | err := Register(config.ExtraConfig{}, nil) 77 | if err != errorNoSecureConfig { 78 | t.Error("expecting errNoConfig. got:", err) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /middleware/melody-httpsecure/httpsecure.go: -------------------------------------------------------------------------------- 1 | package httpsecure 2 | 3 | import ( 4 | "melody/config" 5 | 6 | "github.com/unrolled/secure" 7 | ) 8 | 9 | // Namespace key of http secure 10 | const Namespace = "melody_httpsecure" 11 | 12 | // GetConfig return a httpsecure config interface 13 | func GetConfig(e config.ExtraConfig) interface{} { 14 | v, ok := e[Namespace] 15 | if !ok { 16 | return nil 17 | } 18 | 19 | t, ok := v.(map[string]interface{}) 20 | if !ok { 21 | return nil 22 | } 23 | 24 | c := secure.Options{} 25 | 26 | setInt64(t, "sts_seconds", &c.STSSeconds) 27 | 28 | setStrings(t, "allowed_hosts", &c.AllowedHosts) 29 | setStrings(t, "host_proxy_headers", &c.HostsProxyHeaders) 30 | 31 | setString(t, "custom_frame_option_value", &c.CustomFrameOptionsValue) 32 | setString(t, "content_security_policy", &c.ContentSecurityPolicy) 33 | setString(t, "public_key", &c.PublicKey) 34 | setString(t, "ssl_host", &c.SSLHost) 35 | setString(t, "referrer_policy", &c.ReferrerPolicy) 36 | 37 | setBool(t, "content_type_nosniff", &c.ContentTypeNosniff) 38 | setBool(t, "browser_xss_filter", &c.BrowserXssFilter) 39 | setBool(t, "is_development", &c.IsDevelopment) 40 | setBool(t, "sts_include_subdomains", &c.STSIncludeSubdomains) 41 | setBool(t, "frame_deny", &c.FrameDeny) 42 | setBool(t, "ssl_redirect", &c.SSLRedirect) 43 | 44 | return c 45 | } 46 | 47 | func setStrings(t map[string]interface{}, key string, s *[]string) { 48 | if v, ok := t[key]; ok { 49 | var result []string 50 | for _, s := range v.([]interface{}) { 51 | if str, ok := s.(string); ok { 52 | result = append(result, str) 53 | } 54 | } 55 | *s = result 56 | } 57 | } 58 | 59 | func setString(t map[string]interface{}, key string, s *string) { 60 | if v, ok := t[key]; ok { 61 | if str, ok := v.(string); ok { 62 | *s = str 63 | } 64 | } 65 | } 66 | 67 | func setBool(t map[string]interface{}, key string, b *bool) { 68 | if v, ok := t[key]; ok { 69 | if str, ok := v.(bool); ok { 70 | *b = str 71 | } 72 | } 73 | } 74 | 75 | func setInt64(t map[string]interface{}, key string, i *int64) { 76 | if v, ok := t[key]; ok { 77 | switch a := v.(type) { 78 | case int64: 79 | *i = a 80 | case int: 81 | *i = int64(a) 82 | case float64: 83 | *i = int64(a) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /middleware/melody-httpsecure/httpsecure_test.go: -------------------------------------------------------------------------------- 1 | package httpsecure 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_SetStrings(t *testing.T) { 8 | m := map[string]interface{}{"hosts": []interface{}{"127.0.0.1", "192.168.1.1"}} 9 | a := []string{} 10 | setStrings(m, "hosts", &a) 11 | if len(a) != 2 || a[0] != "127.0.0.1" { 12 | t.Error("set string array error", a) 13 | } 14 | } 15 | 16 | func Test_SetString(t *testing.T) { 17 | m := map[string]interface{}{"header": "yes"} 18 | var a string 19 | setString(m, "header", &a) 20 | if a != "yes" { 21 | t.Error("set string error", a) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/buffer.go: -------------------------------------------------------------------------------- 1 | package influxdb 2 | 3 | import "github.com/influxdata/influxdb/client/v2" 4 | 5 | func NewBuffer(size int) *Buffer { 6 | return &Buffer{ 7 | data: []client.BatchPoints{}, 8 | size: size, 9 | } 10 | } 11 | 12 | type Buffer struct { 13 | data []client.BatchPoints 14 | size int 15 | } 16 | 17 | func (b *Buffer) Add(ps ...client.BatchPoints) { 18 | b.data = append(b.data, ps...) 19 | if len(b.data) > b.size { 20 | b.data = b.data[len(b.data)-b.size:] 21 | } 22 | } 23 | 24 | func (b *Buffer) Elements() []client.BatchPoints { 25 | var res []client.BatchPoints 26 | res, b.data = b.data, []client.BatchPoints{} 27 | return res 28 | } 29 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/config.go: -------------------------------------------------------------------------------- 1 | package influxdb 2 | 3 | import ( 4 | "errors" 5 | "melody/config" 6 | "time" 7 | ) 8 | 9 | const ( 10 | Namespace = "melody_influxdb" 11 | defaultDB = "melody_data" 12 | defaultAddress = "*:8086" 13 | dataServerDefaultListenPort = ":8001" 14 | dataServerDefaultWebSocketPort = ":8002" 15 | ) 16 | 17 | var configErr = errors.New("load influx config error") 18 | 19 | // influxdbConfig 描述metrics输出的influxDB的信息 20 | type influxdbConfig struct { 21 | address string 22 | username string 23 | password string 24 | ttl time.Duration 25 | db string 26 | bufferSize int 27 | timeout time.Duration 28 | dataServerEnable bool 29 | dataServerPort string 30 | dataServerQueryEnable bool 31 | } 32 | 33 | func getConfig(config config.ExtraConfig) interface{} { 34 | v, ok := config[Namespace] 35 | if !ok { 36 | return nil 37 | } 38 | 39 | mapStruct, ok := v.(map[string]interface{}) 40 | if !ok { 41 | return nil 42 | } 43 | 44 | influx := influxdbConfig{} 45 | 46 | if value, ok := mapStruct["address"]; ok { 47 | influx.address = value.(string) 48 | } else { 49 | influx.address = defaultAddress 50 | } 51 | 52 | if value, ok := mapStruct["username"]; ok { 53 | influx.username = value.(string) 54 | } 55 | 56 | if value, ok := mapStruct["password"]; ok { 57 | influx.password = value.(string) 58 | } 59 | 60 | if value, ok := mapStruct["data_server_enable"]; ok { 61 | influx.dataServerEnable = value.(bool) 62 | } 63 | 64 | if value, ok := mapStruct["data_server_query_enable"]; ok { 65 | influx.dataServerQueryEnable = value.(bool) 66 | } 67 | 68 | if value, ok := mapStruct["data_server_port"]; ok || value != "" { 69 | influx.dataServerPort = value.(string) 70 | } else { 71 | influx.dataServerPort = dataServerDefaultListenPort 72 | } 73 | 74 | if value, ok := mapStruct["db"]; ok { 75 | influx.db = value.(string) 76 | } else { 77 | influx.db = defaultDB 78 | } 79 | 80 | if size, ok := mapStruct["buffer_size"]; ok { 81 | if s, ok := size.(int); ok { 82 | influx.bufferSize = s 83 | } 84 | } 85 | 86 | if ttl, ok := mapStruct["ttl"]; ok { 87 | t, ok := ttl.(string) 88 | if !ok { 89 | return nil 90 | } 91 | var err error 92 | influx.ttl, err = time.ParseDuration(t) 93 | if err != nil { 94 | return nil 95 | } 96 | } 97 | 98 | if timeout, ok := mapStruct["time_out"]; ok { 99 | t, ok := timeout.(string) 100 | if !ok { 101 | return nil 102 | } 103 | var err error 104 | influx.timeout, err = time.ParseDuration(t) 105 | if err != nil { 106 | return nil 107 | } 108 | } 109 | 110 | return influx 111 | } 112 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/gauge/gauge.go: -------------------------------------------------------------------------------- 1 | package gauge 2 | 3 | import ( 4 | "melody/logging" 5 | alert "melody/middleware/melody-alert" 6 | "time" 7 | 8 | "github.com/influxdata/influxdb/client/v2" 9 | ) 10 | 11 | func Points(hostname string, now time.Time, counters map[string]int64, logger logging.Logger, checker alert.Checker) []*client.Point { 12 | res := make([]*client.Point, 4) 13 | 14 | in := map[string]interface{}{ 15 | "gauge": int(counters["melody.router.connected-gauge"]), 16 | } 17 | incoming, err := client.NewPoint("router", map[string]string{"host": hostname, "direction": "in"}, in, now) 18 | if err != nil { 19 | logger.Error("creating incoming connection counters point:", err.Error()) 20 | return res 21 | } 22 | res[0] = incoming 23 | 24 | out := map[string]interface{}{ 25 | "gauge": int(counters["melody.router.disconnected-gauge"]), 26 | } 27 | outgoing, err := client.NewPoint("router", map[string]string{"host": hostname, "direction": "out"}, out, now) 28 | if err != nil { 29 | logger.Error("creating outgoing connection counters point:", err.Error()) 30 | return res 31 | } 32 | res[1] = outgoing 33 | 34 | debug := map[string]interface{}{} 35 | runtime := map[string]interface{}{} 36 | 37 | for k, v := range counters { 38 | if k == "melody.router.connected-gauge" || k == "melody.router.disconnected-gauge" { 39 | continue 40 | } 41 | if k[:21] == "melody.service.debug." { 42 | debug[k[21:]] = int(v) 43 | continue 44 | } 45 | if k[:23] == "melody.service.runtime." { 46 | runtime[k[23:]] = int(v) 47 | continue 48 | } 49 | logger.Debug("unknown gauge key:", k) 50 | } 51 | 52 | debugPoint, err := client.NewPoint("debug", map[string]string{"host": hostname}, debug, now) 53 | if err != nil { 54 | logger.Error("creating debug counters point:", err.Error()) 55 | return res 56 | } 57 | res[2] = debugPoint 58 | 59 | runtimePoint, err := client.NewPoint("runtime", map[string]string{"host": hostname}, runtime, now) 60 | if err != nil { 61 | logger.Error("creating runtime counters point:", err.Error()) 62 | return res 63 | } 64 | res[3] = runtimePoint 65 | 66 | _ = checker("", "numgc", int64(debug["GCStats.NumGC"].(int))) 67 | _ = checker("", "numgoroutine", int64(runtime["NumGoroutine"].(int))) 68 | _ = checker("", "sys", int64(runtime["MemStats.Sys"].(int))) 69 | _ = checker("", "heapsys", int64(runtime["MemStats.HeapSys"].(int))) 70 | _ = checker("", "stacksys", int64(runtime["MemStats.StackSys"].(int))) 71 | _ = checker("", "mcachesys", int64(runtime["MemStats.MCacheSys"].(int))) 72 | _ = checker("", "mspansys", int64(runtime["MemStats.MSpanSys"].(int))) 73 | 74 | return res 75 | } 76 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func Cors() gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | method := c.Request.Method 11 | c.Header("Access-Control-Allow-Origin", "*") 12 | c.Header("Access-Control-Allow-Methods", "*") 13 | c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token") 14 | c.Header("Access-Control-Allow-Credentials", "true") 15 | 16 | if method == "OPTIONS" { 17 | c.AbortWithStatus(http.StatusNoContent) 18 | } 19 | c.Next() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/refresh/refresh.go: -------------------------------------------------------------------------------- 1 | package refresh 2 | 3 | import "sync" 4 | 5 | var RefreshList = NewRefreshList() 6 | 7 | type List struct { 8 | Head *Refresh 9 | Tail *Refresh 10 | Size int 11 | lock sync.Mutex 12 | } 13 | 14 | type Refresh struct { 15 | Value *chan int 16 | Next *Refresh 17 | Pre *Refresh 18 | } 19 | 20 | func NewRefreshList() *List { 21 | head := &Refresh{} 22 | tail := &Refresh{ 23 | Pre: head, 24 | } 25 | head.Next = tail 26 | return &List{ 27 | Head: head, 28 | Tail: tail, 29 | } 30 | } 31 | 32 | func (this *List) Add(node *Refresh) { 33 | this.lock.Lock() 34 | node.Pre, node.Next = this.Tail.Pre, this.Tail 35 | node.Pre.Next, node.Next.Pre = node, node 36 | this.Size++ 37 | this.lock.Unlock() 38 | } 39 | 40 | func (this *List) Remove(node *Refresh) { 41 | this.lock.Lock() 42 | node.Pre.Next, node.Next.Pre = node.Next, node.Pre 43 | this.Size-- 44 | this.lock.Unlock() 45 | } 46 | 47 | func (this *List) Front() *Refresh { 48 | return this.Head.Next 49 | } 50 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | type Response struct { 9 | Code int `json:"code"` 10 | Message string `json:"message"` 11 | Data interface{} `json:"data"` 12 | } 13 | 14 | func Ok(c *gin.Context, code int, message string, data interface{}) { 15 | c.JSON(http.StatusOK, Response{ 16 | Code: code, 17 | Message: message, 18 | Data: data, 19 | }) 20 | } 21 | 22 | func Bad(c *gin.Context, message string, data interface{}) { 23 | c.JSON(http.StatusBadRequest, Response{ 24 | Code: http.StatusBadRequest, 25 | Message: message, 26 | Data: data, 27 | }) 28 | } 29 | 30 | func NotFound(c *gin.Context, message string, data interface{}) { 31 | c.JSON(http.StatusNotFound, Response{ 32 | Code: http.StatusNotFound, 33 | Message: message, 34 | Data: data, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/alert_handler.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "melody/middleware/melody-alert/model" 5 | "net/http" 6 | "strconv" 7 | ) 8 | 9 | func (wsc WebSocketClient) GetWarnings() http.HandlerFunc { 10 | return wsc.WebSocketHandler(func(request *http.Request, data map[string]interface{}) (i interface{}, err error) { 11 | pageIndex := 0 12 | if tmp, ok := data["message"]; ok { 13 | if tmp2, ok := tmp.(string); ok { 14 | atoi, _ := strconv.Atoi(tmp2) 15 | pageIndex = atoi - 1 16 | } 17 | } 18 | 19 | total := len(model.WarningList.Warnings) 20 | 21 | start := total - (pageIndex+1)*10 22 | end := total - pageIndex*10 23 | 24 | if start < 0 { 25 | start = 0 26 | } 27 | 28 | result := make([]model.Warning, 0) 29 | 30 | for i := end - 1; i >= start; i-- { 31 | result = append(result, model.WarningList.Warnings[i]) 32 | } 33 | 34 | return map[string]interface{}{ 35 | "warnings": result, 36 | "total": len(result), 37 | }, nil 38 | }) 39 | } 40 | 41 | func (wsc WebSocketClient) WarningsWatch() http.HandlerFunc { 42 | return wsc.WebSocketWatchHandler(func(request *http.Request, data map[string]interface{}) (i interface{}, err error) { 43 | warning := data["warning"] 44 | return map[string]interface{}{ 45 | "warning": warning, 46 | }, nil 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/convert/convert.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | func ObjectToStringTime(v interface{}, format string) (string, bool) { 9 | if n, ok := v.(json.Number); ok { 10 | if ti, err := n.Int64(); err == nil { 11 | t := time.Unix(ti, 0).Format(format) 12 | return t, true 13 | } 14 | } 15 | return "", false 16 | } 17 | 18 | func ObjectToInt(v interface{}) (int64, bool) { 19 | if n, ok := v.(json.Number); ok { 20 | if ti, err := n.Int64(); err == nil { 21 | return ti, true 22 | } 23 | } 24 | return 0, false 25 | } 26 | 27 | func ObjectToFloat(v interface{}) (float64, bool) { 28 | if n, ok := v.(json.Number); ok { 29 | if ti, err := n.Float64(); err == nil { 30 | return ti, true 31 | } 32 | } 33 | return 0, false 34 | } 35 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/debug_handler.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "errors" 5 | "melody/middleware/melody-influxdb/ws/convert" 6 | "melody/middleware/melody-influxdb/ws/handler" 7 | "net/http" 8 | ) 9 | 10 | func (wsc WebSocketClient) GetDebugNumGC() http.HandlerFunc { 11 | return wsc.WebSocketHandler(func(request *http.Request, data map[string]interface{}) (interface{}, error) { 12 | cmd := wsc.generateCommand(`SELECT sum("GCStats.NumGC") 13 | AS "GCStats.NumGC" FROM "%s"."autogen"."debug" WHERE time > %s - %s AND time < 14 | %s GROUP BY time(%s) FILL(null)`) 15 | resu, err := wsc.executeQuery(cmd) 16 | if err != nil { 17 | return nil, err 18 | } 19 | result := resu[0] 20 | if result.Err != "" { 21 | return nil, errors.New(result.Err) 22 | } 23 | values := result.Series[0].Values 24 | var times []string 25 | var debugNumGC []float64 26 | for _, v := range values { 27 | // time 28 | if t, ok := convert.ObjectToStringTime(v[0], GetTimeFormat()); ok { 29 | times = append(times, t) 30 | } else { 31 | continue 32 | } 33 | // value 34 | if f, ok := convert.ObjectToFloat(v[1]); ok { 35 | debugNumGC = append(debugNumGC, f) 36 | } else { 37 | debugNumGC = append(debugNumGC, 0) 38 | } 39 | } 40 | 41 | return map[string]interface{}{ 42 | "title": "debug.NumGC", 43 | "times": times, 44 | "series": []map[string]interface{}{ 45 | { 46 | "data": debugNumGC, 47 | "name": "GCNum", 48 | "type": "line", 49 | "smooth": true, 50 | }, 51 | }, 52 | }, nil 53 | }) 54 | } 55 | 56 | func (wsc WebSocketClient) GetDebugAlloc() http.HandlerFunc { 57 | return wsc.WebSocketHandler(func(request *http.Request, data map[string]interface{}) (interface{}, error) { 58 | cmd := wsc.generateCommand(` 59 | SELECT 60 | sum("MemStats.Alloc") AS "sum_MemStats.Alloc" 61 | FROM 62 | "%s"."autogen"."runtime" 63 | WHERE 64 | time > %s - %s AND time < %s 65 | GROUP BY 66 | time(%s) FILL(null) 67 | `) 68 | resu, err := wsc.executeQuery(cmd) 69 | if err != nil { 70 | return nil, err 71 | } 72 | result := resu[0] 73 | if result.Err != "" { 74 | return nil, errors.New(result.Err) 75 | } 76 | values := result.Series[0].Values 77 | var times []string 78 | var alloc []int64 79 | handler.ResultDataHandler(×, values, GetTimeFormat(), &alloc) 80 | return map[string]interface{}{ 81 | "title": "debug.Alloc", 82 | "times": times, 83 | "series": []map[string]interface{}{ 84 | { 85 | "data": alloc, 86 | "name": "alloc", 87 | "type": "line", 88 | "areaStyle": map[string]interface{}{}, 89 | }, 90 | }, 91 | }, nil 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/handler/chart.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "melody/middleware/melody-influxdb/ws/convert" 6 | ) 7 | 8 | /** 9 | 将InfluxDB返回的结果集映射到数组中 10 | times: 时间数组 11 | data: InfluxDB返回的结果集 12 | format: 时间的格式 13 | lines: 其他数据数组 (一个或以上) 14 | */ 15 | func ResultDataHandler(times *[]string, data [][]interface{}, format string, lines ...*[]int64) { 16 | for _, v := range data { // v:[ time , data1 ,data2] 17 | if t, ok := convert.ObjectToStringTime(v[0], format); ok { 18 | *times = append(*times, t) 19 | } 20 | for index := range lines { 21 | if g, ok := convert.ObjectToInt(v[index+1]); ok { 22 | *lines[index] = append(*lines[index], g) 23 | } else { 24 | *lines[index] = append(*lines[index], 0) 25 | } 26 | } 27 | } 28 | } 29 | 30 | func ResultSingleDataHandler(times *[]string, data [][]interface{}, format string, singleField *int64) { 31 | for _, v := range data { 32 | if t, ok := convert.ObjectToStringTime(v[0], format); ok { 33 | *times = append(*times, t) 34 | } 35 | if vv, ok := v[1].(json.Number); ok { 36 | *singleField, _ = vv.Int64() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/query.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "errors" 5 | "github.com/influxdata/influxdb/client/v2" 6 | ) 7 | 8 | func (wsc WebSocketClient) executeQuery(cmd string) ([]client.Result, error) { 9 | resp, err := wsc.Client.Query(client.NewQuery(cmd, wsc.DB, "s")) 10 | if err != nil { 11 | return nil, err 12 | } 13 | if resp == nil { 14 | return nil, errors.New("error: influx query no resp") 15 | } 16 | if resp.Err != "" { 17 | return nil, errors.New(resp.Err) 18 | } 19 | if len(resp.Results) == 0 { 20 | return nil, errors.New("error: influx query no resp") 21 | } 22 | return resp.Results, nil 23 | } 24 | 25 | func NormalExecuteQuery(c client.Client, cmd, db string) ([]client.Result, error) { 26 | resp, err := c.Query(client.NewQuery(cmd, db, "s")) 27 | if err != nil { 28 | return nil, err 29 | } 30 | if resp == nil { 31 | return nil, errors.New("error: influx query no resp") 32 | } 33 | if resp.Err != "" { 34 | return nil, errors.New(resp.Err) 35 | } 36 | if len(resp.Results) == 0 { 37 | return nil, errors.New("error: influx query no resp") 38 | } 39 | return resp.Results, nil 40 | } 41 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/struct.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "github.com/influxdata/influxdb/client/v2" 6 | "melody/config" 7 | "melody/logging" 8 | "net/http" 9 | ) 10 | 11 | type WebSocketClient struct { 12 | Client client.Client 13 | Upgrader websocket.Upgrader 14 | Logger logging.Logger 15 | DB string 16 | Cfg *config.ServiceConfig 17 | } 18 | 19 | func (wsc WebSocketClient) RegisterHandleFunc() { 20 | http.HandleFunc("/debug/num/gc", wsc.GetDebugNumGC()) 21 | http.HandleFunc("/debug/alloc", wsc.GetDebugAlloc()) 22 | 23 | http.HandleFunc("/runtime/num/gc", wsc.GetNumGC()) 24 | http.HandleFunc("/runtime/num/goroutine", wsc.GetNumGoroutine()) 25 | http.HandleFunc("/runtime/num/frees", wsc.GetNumMemoryFree()) 26 | http.HandleFunc("/runtime/num/memory", wsc.GetSysMemory()) 27 | 28 | http.HandleFunc("/requests/complete", wsc.GetRequestsComplete()) 29 | http.HandleFunc("/requests/error", wsc.GetRequestsError()) 30 | http.HandleFunc("/requests/endpoints", wsc.GetRequestsEndpoints()) 31 | http.HandleFunc("/requests/backends", wsc.GetRequestsBackends()) 32 | http.HandleFunc("/requests/api", wsc.GetRequestsAPI()) 33 | http.HandleFunc("/requests/endpoints/pie", wsc.GetRequestsEndpointsPie()) 34 | http.HandleFunc("/requests/backends/pie", wsc.GetRequestsBackendsPie()) 35 | 36 | http.HandleFunc("/router/direction", wsc.GetRouterDirection()) 37 | http.HandleFunc("/router/size", wsc.GetRouterSize()) 38 | http.HandleFunc("/router/time", wsc.GetRouterTime()) 39 | 40 | http.HandleFunc("/warnings", wsc.GetWarnings()) 41 | http.HandleFunc("/warnings/watch", wsc.WarningsWatch()) 42 | } 43 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/template.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "fmt" 5 | "melody/config" 6 | "strings" 7 | ) 8 | 9 | func (wsc WebSocketClient) generateCommand(cmd string) string { 10 | return fmt.Sprintf(cmd, wsc.DB, WsTimeControl.MinTime, WsTimeControl.TimeInterval, WsTimeControl.MaxTime, WsTimeControl.GroupTime) 11 | } 12 | 13 | func (wsc WebSocketClient) generateCommandWithSingle(cmd string) string { 14 | return fmt.Sprintf(cmd, wsc.DB, WsTimeControl.MinTime, WsTimeControl.TimeInterval, WsTimeControl.MaxTime) 15 | } 16 | 17 | func (wsc WebSocketClient) generateCommandWithEndpoints(cmd string, cfg *config.ServiceConfig) string { 18 | var endpointStr []string 19 | for i := range cfg.Endpoints { 20 | endpointStr = append(endpointStr, cfg.Endpoints[i].Endpoint) 21 | } 22 | builder := strings.Builder{} 23 | builder.WriteString("(") 24 | for i := 0; i < len(endpointStr); i++ { 25 | builder.WriteString(`"name"='`) 26 | builder.WriteString(endpointStr[i]) 27 | if i == len(endpointStr)-1 { 28 | builder.WriteString("'") 29 | } else { 30 | builder.WriteString("' OR ") 31 | } 32 | } 33 | builder.WriteString(")") 34 | return fmt.Sprintf( 35 | cmd, 36 | wsc.DB, 37 | WsTimeControl.MinTime, 38 | WsTimeControl.TimeInterval, 39 | WsTimeControl.MaxTime, 40 | builder.String(), 41 | WsTimeControl.GroupTime, 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /middleware/melody-influxdb/ws/timer.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "regexp" 5 | "time" 6 | ) 7 | 8 | type TimeControl struct { 9 | MinTime string `json:"min_time"` 10 | MaxTime string `json:"max_time"` 11 | TimeInterval string `json:"time_interval"` 12 | GroupTime string `json:"group_time"` 13 | RefreshTime time.Duration 14 | RefreshParam string `json:"refresh_time"` 15 | } 16 | 17 | var WsTimeControl TimeControl 18 | 19 | func RegisterWSTimeControl() { 20 | WsTimeControl = TimeControl{ 21 | MinTime: "now()", 22 | MaxTime: "now()", 23 | TimeInterval: "15m", 24 | GroupTime: "10s", 25 | RefreshTime: 10 * time.Second, 26 | } 27 | } 28 | 29 | func SetTimeControl(timer TimeControl) { 30 | WsTimeControl = timer 31 | } 32 | 33 | var r = regexp.MustCompile(`([0-9]*)([a-z])`) 34 | 35 | const ( 36 | hour = "15:04" 37 | day = "01-02 15:04" 38 | ) 39 | 40 | func GetTimeFormat() string { 41 | if WsTimeControl.TimeInterval != "" { 42 | match := r.FindAllStringSubmatch(WsTimeControl.TimeInterval, -1) 43 | m := match[0] 44 | if len(m) == 3 { 45 | unit := m[2] 46 | if unit == "d" { 47 | return day 48 | } 49 | } 50 | } 51 | return hour 52 | } 53 | -------------------------------------------------------------------------------- /middleware/melody-jose/fixtures/jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "kty": "RSA", 6 | "use": "sig", 7 | "n": "s3X0UDM3Us5ESCKIiUHK6kgWe4utsH4-NM4LuNOJK4puamtXQRqr786U2T1bhxhIxmRqsGaIOu1bPLUuha-vuqEnQTL4SC-1LzHYt5Q1booyk01jZsBCnhJiOyJ1Y20DKiPJK3g7Zk9Qa8ELhjlx4XDkkSxuQOnKSpqnz-qjgEivX4IdVaGgbtAMY9Wk68QiTYwEWNh8GGLWJY6iIX-6YZ5YISaFSdshouvuflPixRVqCugrnetgbiOEVT3onx2rnPKWqKWnW73J2anFReGubX57dyKY2rDiz00ME6uMH3aBMzh1TSdKvQN6gihVNE3937Mefp9Hn5DSI1oMe7PAEw", 8 | "e": "AQAB", 9 | "kid": "RjA2MTU1NjgyMzFDRDhGMTlGQkQ0RUM5MUJGMkQzQzBCNEYwNTczMQ", 10 | "x5t": "RjA2MTU1NjgyMzFDRDhGMTlGQkQ0RUM5MUJGMkQzQzBCNEYwNTczMQ", 11 | "x5c": [ 12 | "MIIC+zCCAeOgAwIBAgIJK4SJPHkUYbLYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMTEHhpYW9jaS5hdXRoMC5jb20wHhcNMjAwMzA2MDQ0NzUzWhcNMzMxMTEzMDQ0NzUzWjAbMRkwFwYDVQQDExB4aWFvY2kuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs3X0UDM3Us5ESCKIiUHK6kgWe4utsH4+NM4LuNOJK4puamtXQRqr786U2T1bhxhIxmRqsGaIOu1bPLUuha+vuqEnQTL4SC+1LzHYt5Q1booyk01jZsBCnhJiOyJ1Y20DKiPJK3g7Zk9Qa8ELhjlx4XDkkSxuQOnKSpqnz+qjgEivX4IdVaGgbtAMY9Wk68QiTYwEWNh8GGLWJY6iIX+6YZ5YISaFSdshouvuflPixRVqCugrnetgbiOEVT3onx2rnPKWqKWnW73J2anFReGubX57dyKY2rDiz00ME6uMH3aBMzh1TSdKvQN6gihVNE3937Mefp9Hn5DSI1oMe7PAEwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ6jdBLLAINDV7ToqWHoM6aFODQ3zAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAFyiSikrVsuiBnPTclnvymqGWRSI/FY7pdf37BaXhp9Ybtld+Lk8kESlEAnWdig5iMeg4T4vqPQCXcFHdLqFCAFm0RYzXzd+h5Bs3U6Qm6wpPXZJRyeZSY/T/qLUtaN7bzx5PO4HyvyNYGIydH4FekDTEKO8VQB+jUk/KBS4v4QjsPnmRtVH6yLpaOZ5NiIJqGJUVUSZ1cCjnbopGt7ySLRUBo4PON9NtGblZMpMO4orZ4uHRKaVuT5PuD1YP+3FWj/yfTB4+K62vIIWc9uCjQwRXsyLtSW+m/BbyeXOm+NzIrIWAOp0nZqIrAyje392+DYxVb+J2hz35Ec5g2Hvp3U=" 13 | ] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /middleware/melody-jose/fixtures/private.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty":"EC", 5 | "crv":"P-256", 6 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 7 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 8 | "d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", 9 | "use":"enc", 10 | "kid":"1" 11 | }, 12 | { 13 | "kty":"RSA", 14 | "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 15 | "e":"AQAB", 16 | "d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", 17 | "p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", 18 | "q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", 19 | "dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", 20 | "dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", 21 | "qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", 22 | "alg":"RS256", 23 | "kid":"2011-04-29" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /middleware/melody-jose/fixtures/public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty":"EC", 5 | "crv":"P-256", 6 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 7 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 8 | "use":"enc", 9 | "kid":"1" 10 | }, 11 | { 12 | "kty":"RSA", 13 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 14 | "e":"AQAB", 15 | "alg":"RS256", 16 | "kid":"2011-04-29" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /middleware/melody-jose/fixtures/symmetric.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "oct", 5 | "alg": "A128KW", 6 | "k": "GawgguFyGrWKav7AX4VKUg", 7 | "kid": "sim1" 8 | }, 9 | { 10 | "kty": "oct", 11 | "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", 12 | "kid": "sim2", 13 | "alg": "HS256" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /middleware/melody-jose/jwk_example_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package jose 4 | 5 | import "fmt" 6 | 7 | func Example_Auth0Integration() { 8 | fs, _ := DecodeFingerprints([]string{"--MBgDH5WGvL9Bcn5Be30cRcL0f5O-NyoXuWtQdX1aI="}) 9 | cfg := SecretProviderConfig{ 10 | URI: "https://albert-test.auth0.com/.well-known/jwks.json", 11 | Fingerprints: fs, 12 | } 13 | client, _ := SecretProvider(cfg, nil) 14 | 15 | k, err := client.GetKey("MDNGMjU2M0U3RERFQUEwOUUzQUMwQ0NBN0Y1RUY0OEIxNTRDM0IxMw") 16 | fmt.Println("err:", err) 17 | fmt.Println("is public:", k.IsPublic()) 18 | fmt.Println("alg:", k.Algorithm) 19 | fmt.Println("id:", k.KeyID) 20 | // Output: 21 | // err: 22 | // is public: true 23 | // alg: RS256 24 | // id: MDNGMjU2M0U3RERFQUEwOUUzQUMwQ0NBN0Y1RUY0OEIxNTRDM0IxMw 25 | } 26 | 27 | func Example_Auth0Integration_badFingerprint() { 28 | cfg := SecretProviderConfig{ 29 | URI: "https://albert-test.auth0.com/.well-known/jwks.json", 30 | Fingerprints: [][]byte{make([]byte, 32)}, 31 | } 32 | client, _ := SecretProvider(cfg, nil) 33 | 34 | _, err := client.GetKey("MDNGMjU2M0U3RERFQUEwOUUzQUMwQ0NBN0Y1RUY0OEIxNTRDM0IxMw") 35 | fmt.Println("err:", err) 36 | // Output: 37 | // err: Get https://albert-test.auth0.com/.well-known/jwks.json: JWK client did not find a pinned key 38 | } 39 | -------------------------------------------------------------------------------- /middleware/melody-jose/jwk_test.go: -------------------------------------------------------------------------------- 1 | //go:generate go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,localhost --ca --start-date "Jan 1 00:00:00 2000" --duration=1000000h 2 | package jose 3 | 4 | import ( 5 | "crypto/tls" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | func TestJWK(t *testing.T) { 13 | cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") 14 | if err != nil { 15 | t.Error(err) 16 | return 17 | } 18 | 19 | for _, tc := range []struct { 20 | Name string 21 | Alg string 22 | ID []string 23 | }{ 24 | { 25 | Name: "public", 26 | ID: []string{"2011-04-29"}, 27 | Alg: "RS256", 28 | }, 29 | { 30 | Name: "public", 31 | ID: []string{"1"}, 32 | }, 33 | { 34 | Name: "private", 35 | ID: []string{"2011-04-29"}, 36 | Alg: "RS256", 37 | }, 38 | { 39 | Name: "private", 40 | ID: []string{"1"}, 41 | }, 42 | { 43 | Name: "symmetric", 44 | ID: []string{"sim2"}, 45 | Alg: "HS256", 46 | }, 47 | } { 48 | server := httptest.NewUnstartedServer(jwkEndpoint(tc.Name)) 49 | defer server.Close() 50 | server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} 51 | server.StartTLS() 52 | 53 | secretProvidr, err := SecretProvider(SecretProviderConfig{URI: server.URL, LocalCA: "cert.pem"}, nil) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | for _, k := range tc.ID { 58 | key, err := secretProvidr.GetKey(k) // 发get请求 59 | if err != nil { 60 | t.Errorf("[%s] extracting the key %s: %s", tc.Name, k, err.Error()) 61 | } 62 | if key.Algorithm != tc.Alg { 63 | t.Errorf("wrong alg. have: %s, want: %s", key.Algorithm, tc.Alg) 64 | } 65 | } 66 | } 67 | } 68 | 69 | func TestDialer_DialTLS_ko(t *testing.T) { 70 | d := NewDialer(SecretProviderConfig{}) 71 | c, err := d.DialTLS("\t", "addr") 72 | if err == nil { 73 | t.Error(err) 74 | } 75 | if c != nil { 76 | t.Errorf("unexpected connection: %v", c) 77 | } 78 | } 79 | 80 | func Test_decodeFingerprints(t *testing.T) { 81 | _, err := DecodeFingerprints([]string{"not_encoded_message"}) 82 | if err == nil { 83 | t.Error(err) 84 | } 85 | } 86 | 87 | func jwkEndpoint(name string) http.HandlerFunc { 88 | data, err := ioutil.ReadFile("./fixtures/" + name + ".json") 89 | return func(rw http.ResponseWriter, _ *http.Request) { 90 | if err != nil { 91 | rw.WriteHeader(500) 92 | return 93 | } 94 | rw.Header().Set("Content-Type", "application/json") 95 | rw.Write(data) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /middleware/melody-jose/rejecter.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "melody/config" 5 | "melody/logging" 6 | ) 7 | 8 | // Rejecter defines the interface for the components responsible for rejecting tokens. 9 | type Rejecter interface { 10 | Reject(map[string]interface{}) bool 11 | } 12 | 13 | // RejecterFunc is an adapter to use functions as rejecters 14 | type RejecterFunc func(map[string]interface{}) bool 15 | 16 | // Reject calls r(v) 17 | func (r RejecterFunc) Reject(v map[string]interface{}) bool { return r(v) } 18 | 19 | // FixedRejecter is a rejecter that always returns the same bool response 20 | type FixedRejecter bool 21 | 22 | // Reject returns f 23 | func (f FixedRejecter) Reject(_ map[string]interface{}) bool { return bool(f) } 24 | 25 | // RejecterFactory is a builder for rejecters 26 | type RejecterFactory interface { 27 | New(logging.Logger, *config.EndpointConfig) Rejecter 28 | } 29 | 30 | // RejecterFactoryFunc is an adapter to use a function as rejecter factory 31 | type RejecterFactoryFunc func(logging.Logger, *config.EndpointConfig) Rejecter 32 | 33 | // New calls f(l, cfg) 34 | func (f RejecterFactoryFunc) New(l logging.Logger, cfg *config.EndpointConfig) Rejecter { 35 | return f(l, cfg) 36 | } 37 | 38 | // NopRejecterFactory is a factory returning rejecters accepting all the tokens 39 | type NopRejecterFactory struct{} 40 | 41 | // New returns a fixed rejecter that accepts all the tokens 42 | func (NopRejecterFactory) New(_ logging.Logger, _ *config.EndpointConfig) Rejecter { 43 | return FixedRejecter(false) 44 | } 45 | 46 | // ChainedRejecterFactory returns rejecters chaining every rejecter contained in tne collection 47 | type ChainedRejecterFactory []RejecterFactory 48 | 49 | // New returns a chainned rejected that evaluates all the rejecters until v is rejected or the chain 50 | // is finished 51 | func (c ChainedRejecterFactory) New(l logging.Logger, cfg *config.EndpointConfig) Rejecter { 52 | rejecters := []Rejecter{} 53 | for _, rf := range c { 54 | rejecters = append(rejecters, rf.New(l, cfg)) 55 | } 56 | return RejecterFunc(func(v map[string]interface{}) bool { 57 | for _, r := range rejecters { 58 | if r.Reject(v) { 59 | return true 60 | } 61 | } 62 | return false 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /middleware/melody-jose/rejecter_test.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "melody/config" 5 | "melody/logging" 6 | "testing" 7 | ) 8 | 9 | func TestChainedRejecterFactory(t *testing.T) { 10 | rf := ChainedRejecterFactory([]RejecterFactory{ 11 | NopRejecterFactory{}, 12 | RejecterFactoryFunc(func(_ logging.Logger, _ *config.EndpointConfig) Rejecter { 13 | return RejecterFunc(func(in map[string]interface{}) bool { 14 | v, ok := in["key"].(int) 15 | return ok && v == 42 16 | }) 17 | }), 18 | }) 19 | 20 | rejecter := rf.New(nil, nil) 21 | 22 | for _, tc := range []struct { 23 | name string 24 | in map[string]interface{} 25 | expected bool 26 | }{ 27 | { 28 | name: "empty", 29 | in: map[string]interface{}{}, 30 | }, 31 | { 32 | name: "reject", 33 | in: map[string]interface{}{"key": 42}, 34 | expected: true, 35 | }, 36 | { 37 | name: "pass-1", 38 | in: map[string]interface{}{"key": "42"}, 39 | }, 40 | { 41 | name: "pass-2", 42 | in: map[string]interface{}{"key": 9876}, 43 | }, 44 | } { 45 | if v := rejecter.Reject(tc.in); tc.expected != v { 46 | t.Errorf("unexpected result for %s. have %v, want %v", tc.name, v, tc.expected) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /middleware/melody-jsonschema/jsonschema.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "melody/config" 11 | "melody/proxy" 12 | "strings" 13 | 14 | "github.com/xeipuuv/gojsonschema" 15 | ) 16 | 17 | const Namespace = "melody_jsonschema" 18 | 19 | var ErrEmptyBody = errors.New("could not validate an empty body") 20 | 21 | // ProxyFactory ... 22 | func ProxyFactory(pf proxy.Factory) proxy.FactoryFunc { 23 | return proxy.FactoryFunc(func(cfg *config.EndpointConfig) (proxy.Proxy, error) { 24 | next, err := pf.New(cfg) 25 | if err != nil { 26 | return proxy.NoopProxy, err 27 | } 28 | schemaLoader, ok := configGetter(cfg.ExtraConfig).(gojsonschema.JSONLoader) 29 | if !ok || schemaLoader == nil { 30 | return next, nil 31 | } 32 | return newProxy(schemaLoader, next), nil 33 | }) 34 | } 35 | 36 | func newProxy(schemaLoader gojsonschema.JSONLoader, next proxy.Proxy) proxy.Proxy { 37 | return func(ctx context.Context, r *proxy.Request) (*proxy.Response, error) { 38 | if r.Body == nil { 39 | return nil, ErrEmptyBody 40 | } 41 | body, err := ioutil.ReadAll(r.Body) 42 | if err != nil { 43 | return nil, err 44 | } 45 | r.Body.Close() 46 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 47 | 48 | result, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewBytesLoader(body)) 49 | if err != nil { 50 | return nil, err 51 | } 52 | if !result.Valid() { 53 | return nil, &validationError{errs: result.Errors()} 54 | } 55 | 56 | return next(ctx, r) 57 | } 58 | } 59 | 60 | func configGetter(cfg config.ExtraConfig) interface{} { 61 | v, ok := cfg[Namespace] 62 | if !ok { 63 | return nil 64 | } 65 | buf := new(bytes.Buffer) 66 | if err := json.NewEncoder(buf).Encode(v); err != nil { 67 | return nil 68 | } 69 | return gojsonschema.NewBytesLoader(buf.Bytes()) 70 | } 71 | 72 | type validationError struct { 73 | errs []gojsonschema.ResultError 74 | } 75 | 76 | func (v *validationError) Error() string { 77 | errs := make([]string, len(v.errs)) 78 | for i, desc := range v.errs { 79 | errs[i] = fmt.Sprintf("- %s", desc) 80 | } 81 | return strings.Join(errs, "\n") 82 | } 83 | -------------------------------------------------------------------------------- /middleware/melody-martian/context.go: -------------------------------------------------------------------------------- 1 | package martian 2 | 3 | import "context" 4 | 5 | // NewContext 返回一个包装了接收到的父类的上下文 6 | func NewContext(parent context.Context) *Context { 7 | return &Context{ 8 | Context: parent, 9 | } 10 | } 11 | 12 | // Context 提供单个请求/响应对的信息。 13 | type Context struct { 14 | context.Context 15 | skipRoundTrip bool 16 | } 17 | 18 | // SkipRoundTrip 标记上下文以跳过往返 19 | func (c *Context) SkipRoundTrip() { 20 | c.skipRoundTrip = true 21 | } 22 | 23 | // SkippingRoundTrip 返回跳过往返旅程的标志 24 | func (c *Context) SkippingRoundTrip() bool { 25 | return c.skipRoundTrip 26 | } 27 | 28 | var _ context.Context = &Context{Context: context.Background()} 29 | -------------------------------------------------------------------------------- /middleware/melody-martian/register.go: -------------------------------------------------------------------------------- 1 | package martian 2 | 3 | import ( 4 | "melody/middleware/melody-martian/register" 5 | 6 | "github.com/google/martian/parse" 7 | ) 8 | 9 | // Register 从 krakend-martian 寄存器获取所有修饰符,并将它们注册到 martian 解析器中 10 | func Register() { 11 | for k, component := range register.Get() { 12 | parse.Register(k, func(b []byte) (*parse.Result, error) { 13 | v, err := component.NewFromJSON(b) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return parse.NewResult(v, toModifierType(component.Scope)) 19 | }) 20 | } 21 | } 22 | 23 | func toModifierType(scopes []register.Scope) []parse.ModifierType { 24 | modifierType := make([]parse.ModifierType, len(scopes)) 25 | for k, s := range scopes { 26 | modifierType[k] = parse.ModifierType(s) 27 | } 28 | return modifierType 29 | } 30 | -------------------------------------------------------------------------------- /middleware/melody-martian/register/register.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import "sync" 4 | 5 | const ( 6 | // ScopeRequest 修改HTTP请求。 7 | ScopeRequest Scope = "request" 8 | // ScopeResponse 修改HTTP响应。 9 | ScopeResponse Scope = "response" 10 | ) 11 | 12 | // Register 是包含所有martian组件的结构体 13 | type Register map[string]Component 14 | 15 | // Scope 定义组件的范围 16 | type Scope string 17 | 18 | // Component 组件包含范围和模块工厂 19 | type Component struct { 20 | Scope []Scope 21 | NewFromJSON func(b []byte) (interface{}, error) 22 | } 23 | 24 | var ( 25 | register = Register{} 26 | mutex = &sync.RWMutex{} 27 | ) 28 | 29 | // Set 将接收到的数据添加到寄存器中 30 | func Set(name string, scope []Scope, f func(b []byte) (interface{}, error)) { 31 | mutex.Lock() 32 | register[name] = Component{ 33 | Scope: scope, 34 | NewFromJSON: f, 35 | } 36 | mutex.Unlock() 37 | } 38 | 39 | // Get 获取寄存器的副本 40 | func Get() Register { 41 | mutex.RLock() 42 | r := make(Register, len(register)) 43 | for k, v := range register { 44 | r[k] = v 45 | } 46 | mutex.RUnlock() 47 | return r 48 | } 49 | -------------------------------------------------------------------------------- /middleware/melody-martian/static.go: -------------------------------------------------------------------------------- 1 | package martian 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/google/martian/parse" 8 | "github.com/google/martian/static" 9 | ) 10 | 11 | // StaticModifier is a martian. 12 | type StaticModifier struct { 13 | *static.Modifier 14 | } 15 | 16 | type staticJSON struct { 17 | ExplicitPaths map[string]string `json:"explicitPaths"` 18 | RootPath string `json:"rootPath"` 19 | Scope []parse.ModifierType `json:"scope"` 20 | } 21 | 22 | // NewStaticModifier 构造一个static.Modifier,它采用一个路径来服务文件,以及一个可选的请求路径到本地文件路径的映射(仍然以rootPath为根)。 23 | func NewStaticModifier(rootPath string) *StaticModifier { 24 | return &StaticModifier{ 25 | Modifier: static.NewModifier(rootPath), 26 | } 27 | } 28 | 29 | // ModifyRequest 将上下文标记为跳过往返,并将所有https请求降级为http。 30 | func (s *StaticModifier) ModifyRequest(req *http.Request) error { 31 | ctx := NewContext(req.Context()) 32 | ctx.SkipRoundTrip() 33 | 34 | if req.URL.Scheme == "https" { 35 | req.URL.Scheme = "http" 36 | } 37 | 38 | *req = *req.WithContext(ctx) 39 | 40 | return nil 41 | } 42 | 43 | func staticModifierFromJSON(b []byte) (*parse.Result, error) { 44 | msg := &staticJSON{} 45 | if err := json.Unmarshal(b, msg); err != nil { 46 | return nil, err 47 | } 48 | 49 | mod := NewStaticModifier(msg.RootPath) 50 | mod.SetExplicitPathMappings(msg.ExplicitPaths) 51 | return parse.NewResult(mod, msg.Scope) 52 | } 53 | -------------------------------------------------------------------------------- /middleware/melody-metrics/router.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/rcrowley/go-metrics" 4 | 5 | type RouterMetrics struct { 6 | ProxyMetrics 7 | connected metrics.Counter 8 | disconnected metrics.Counter 9 | connectedTotal metrics.Counter 10 | disconnectedTotal metrics.Counter 11 | connectedGauge metrics.Gauge 12 | disconnectedGauge metrics.Gauge 13 | } 14 | 15 | func NewRouterMetrics(parent *metrics.Registry) *RouterMetrics { 16 | r := metrics.NewPrefixedChildRegistry(*parent, "router.") 17 | 18 | return &RouterMetrics{ 19 | ProxyMetrics: ProxyMetrics{register: r}, 20 | connected: metrics.NewRegisteredCounter("connected", r), 21 | disconnected: metrics.NewRegisteredCounter("disconnected", r), 22 | connectedTotal: metrics.NewRegisteredCounter("connected-total", r), 23 | disconnectedTotal: metrics.NewRegisteredCounter("disconnected-total", r), 24 | connectedGauge: metrics.NewRegisteredGauge("connected-gauge", r), 25 | disconnectedGauge: metrics.NewRegisteredGauge("disconnected-gauge", r), 26 | } 27 | } 28 | 29 | func (rm *RouterMetrics) Aggregate() { 30 | con := rm.connected.Count() 31 | rm.connectedGauge.Update(con) 32 | rm.connectedTotal.Inc(con) 33 | rm.connected.Clear() 34 | discon := rm.disconnected.Count() 35 | rm.disconnectedGauge.Update(discon) 36 | rm.disconnectedTotal.Inc(discon) 37 | rm.disconnected.Clear() 38 | } 39 | 40 | func (rm *RouterMetrics) Disconnection() { 41 | rm.disconnected.Inc(1) 42 | } 43 | 44 | func (rm *RouterMetrics) RegisterResponseWriterMetrics(name string) { 45 | rm.Counter("response", name, "status") 46 | rm.Histogram("response", name, "size") 47 | rm.Histogram("response", name, "time") 48 | } 49 | -------------------------------------------------------------------------------- /middleware/melody-metrics/stats.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "time" 4 | 5 | func NewStats() Stats { 6 | return Stats{ 7 | Time: time.Now().UnixNano(), 8 | Counters: map[string]int64{}, 9 | Gauges: map[string]int64{}, 10 | Histograms: map[string]HistogramData{}, 11 | } 12 | } 13 | 14 | type Stats struct { 15 | Time int64 16 | Counters map[string]int64 17 | Gauges map[string]int64 18 | Histograms map[string]HistogramData 19 | } 20 | 21 | // HistogramData is a snapshot of an actual histogram 22 | type HistogramData struct { 23 | Max int64 24 | Min int64 25 | Mean float64 26 | Stddev float64 27 | Variance float64 28 | Percentiles []float64 29 | } 30 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/juju/juju.go: -------------------------------------------------------------------------------- 1 | package juju 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/juju/ratelimit" 7 | 8 | melodyrate "melody/middleware/melody-ratelimit" 9 | ) 10 | 11 | // NewLimiter 创建了一个新的 Limiter 12 | func NewLimiter(maxRate float64, capacity int64) Limiter { 13 | return Limiter{ratelimit.NewBucketWithRate(maxRate, capacity)} 14 | } 15 | 16 | // Limiter 是对 ratelimit.Bucket struct 的简单包装 17 | type Limiter struct { 18 | limiter *ratelimit.Bucket 19 | } 20 | 21 | // Allow 检查是否可以从 bucket 中提取一个token 22 | func (l Limiter) Allow() bool { 23 | return l.limiter.TakeAvailable(1) > 0 24 | } 25 | 26 | // NewLimiterStore 使用接收的后端返回一个用于持久性的LimiterStore 27 | func NewLimiterStore(maxRate float64, capacity int64, backend melodyrate.Backend) melodyrate.LimiterStore { 28 | f := func() interface{} { return NewLimiter(maxRate, capacity) } 29 | return func(t string) melodyrate.Limiter { 30 | return backend.Load(t, f).(Limiter) 31 | } 32 | } 33 | 34 | // NewMemoryStore 使用内存后端返回一个 LimiterStore 35 | func NewMemoryStore(maxRate float64, capacity int64) melodyrate.LimiterStore { 36 | return NewLimiterStore(maxRate, capacity, melodyrate.DefaultShardedMemoryBackend(context.Background())) 37 | } 38 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/juju/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | 6 | "melody/config" 7 | "melody/proxy" 8 | 9 | melodyrate "melody/middleware/melody-ratelimit" 10 | "melody/middleware/melody-ratelimit/juju" 11 | ) 12 | 13 | // Namespace 命名空间 14 | const Namespace = "melody_ratelimit_proxy" 15 | 16 | // Config 是包含限定符参数的自定义配置结构 17 | type Config struct { 18 | MaxRate float64 19 | Capacity int64 20 | } 21 | 22 | // BackendFactory 添加了一个包装内部工厂的速率限制中间件 23 | func BackendFactory(next proxy.BackendFactory) proxy.BackendFactory { 24 | return func(cfg *config.Backend) proxy.Proxy { 25 | return NewMiddleware(cfg)(next(cfg)) 26 | } 27 | } 28 | 29 | // NewMiddleware 基于对下一个代理的额外配置参数或回退构建中间件 30 | func NewMiddleware(remote *config.Backend) proxy.Middleware { 31 | cfg := ConfigGetter(remote.ExtraConfig).(Config) 32 | if cfg == ZeroCfg || cfg.MaxRate <= 0 { 33 | return proxy.EmptyMiddleware 34 | } 35 | tb := juju.NewLimiter(cfg.MaxRate, cfg.Capacity) 36 | return func(next ...proxy.Proxy) proxy.Proxy { 37 | if len(next) > 1 { 38 | panic(proxy.ErrTooManyProxies) 39 | } 40 | return func(ctx context.Context, request *proxy.Request) (*proxy.Response, error) { 41 | if !tb.Allow() { 42 | return nil, melodyrate.ErrLimited 43 | } 44 | return next[0](ctx, request) 45 | } 46 | } 47 | } 48 | 49 | // ZeroCfg 是配置结构的空值 50 | var ZeroCfg = Config{} 51 | 52 | //ConfigGetter 实现config.ConfigGetter接口。它解析速率适配器的额外配置,如果出了问题,则返回一个ZeroCfg。 53 | func ConfigGetter(e config.ExtraConfig) interface{} { 54 | v, ok := e[Namespace] 55 | if !ok { 56 | return ZeroCfg 57 | } 58 | tmp, ok := v.(map[string]interface{}) 59 | if !ok { 60 | return ZeroCfg 61 | } 62 | cfg := Config{} 63 | if v, ok := tmp["maxRate"]; ok { 64 | switch val := v.(type) { 65 | case float64: 66 | cfg.MaxRate = val 67 | case int: 68 | cfg.MaxRate = float64(val) 69 | case int64: 70 | cfg.MaxRate = float64(val) 71 | } 72 | } 73 | if v, ok := tmp["capacity"]; ok { 74 | switch val := v.(type) { 75 | case int64: 76 | cfg.Capacity = val 77 | case int: 78 | cfg.Capacity = int64(val) 79 | case float64: 80 | cfg.Capacity = int64(val) 81 | } 82 | } 83 | return cfg 84 | } 85 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/juju/proxy/proxy_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "melody/config" 8 | "melody/proxy" 9 | ) 10 | 11 | func BenchmarkNewMiddleware_ok(b *testing.B) { 12 | p := NewMiddleware(&config.Backend{ 13 | ExtraConfig: map[string]interface{}{Namespace: map[string]interface{}{"maxRate": 10000000000000.0, "capacity": 100000000000.0}}, 14 | })(dummyProxy(&proxy.Response{}, nil)) 15 | b.ResetTimer() 16 | for i := 0; i < b.N; i++ { 17 | p(context.Background(), &proxy.Request{ 18 | Path: "/tupu", 19 | }) 20 | } 21 | } 22 | 23 | func BenchmarkNewCMiddleware_ko(b *testing.B) { 24 | p := NewMiddleware(&config.Backend{ 25 | ExtraConfig: map[string]interface{}{Namespace: map[string]interface{}{"maxRate": 1.0, "capacity": 1.0}}, 26 | })(dummyProxy(&proxy.Response{}, nil)) 27 | b.ResetTimer() 28 | for i := 0; i < b.N; i++ { 29 | p(context.Background(), &proxy.Request{ 30 | Path: "/tupu", 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/juju/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "melody/config" 9 | 10 | "melody/proxy" 11 | 12 | melodyrate "melody/middleware/melody-ratelimit" 13 | ) 14 | 15 | func TestNewMiddleware_multipleNext(t *testing.T) { 16 | defer func() { 17 | if r := recover(); r != proxy.ErrTooManyProxies { 18 | t.Errorf("The code did not panic\n") 19 | } 20 | }() 21 | NewMiddleware(&config.Backend{})(proxy.NoopProxy, proxy.NoopProxy) 22 | } 23 | 24 | func TestNewMiddleware_zeroConfig(t *testing.T) { 25 | for _, cfg := range []*config.Backend{ 26 | {}, 27 | {ExtraConfig: map[string]interface{}{Namespace: 42}}, 28 | } { 29 | resp := proxy.Response{} 30 | mdw := NewMiddleware(cfg) 31 | p := mdw(dummyProxy(&resp, nil)) 32 | 33 | request := proxy.Request{ 34 | Path: "/tupu", 35 | } 36 | 37 | for i := 0; i < 100; i++ { 38 | r, err := p(context.Background(), &request) 39 | if err != nil { 40 | t.Error(err.Error()) 41 | return 42 | } 43 | if &resp != r { 44 | t.Fail() 45 | } 46 | } 47 | } 48 | } 49 | 50 | func TestNewMiddleware_ok(t *testing.T) { 51 | resp := proxy.Response{} 52 | mdw := NewMiddleware(&config.Backend{ 53 | ExtraConfig: map[string]interface{}{Namespace: map[string]interface{}{"maxRate": 10000.0, "capacity": 10000.0}}, 54 | }) 55 | p := mdw(dummyProxy(&resp, nil)) 56 | 57 | request := proxy.Request{ 58 | Path: "/tupu", 59 | } 60 | 61 | for i := 0; i < 1000; i++ { 62 | r, err := p(context.Background(), &request) 63 | if err != nil { 64 | t.Error(err.Error()) 65 | return 66 | } 67 | if &resp != r { 68 | t.Fail() 69 | } 70 | } 71 | } 72 | 73 | func TestNewMiddleware_ko(t *testing.T) { 74 | expected := proxy.Response{} 75 | calls := uint64(0) 76 | mdw := NewMiddleware(&config.Backend{ 77 | ExtraConfig: map[string]interface{}{Namespace: map[string]interface{}{"maxRate": 1.0, "capacity": 1.0}}, 78 | }) 79 | p := mdw(func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 80 | total := atomic.AddUint64(&calls, 1) 81 | if total > 2 { 82 | t.Error("This proxy shouldn't been executed!") 83 | } 84 | return &expected, nil 85 | }) 86 | 87 | request := proxy.Request{ 88 | Path: "/tupu", 89 | } 90 | 91 | for i := 0; i < 100; i++ { 92 | p(context.Background(), &request) 93 | } 94 | 95 | r, err := p(context.Background(), &request) 96 | if err != melodyrate.ErrLimited { 97 | t.Errorf("error expected") 98 | } 99 | if nil != r { 100 | t.Error("unexpected response") 101 | } 102 | if calls != 1 { 103 | t.Error("unexpected number of calls to the proxy") 104 | } 105 | } 106 | 107 | func dummyProxy(r *proxy.Response, err error) proxy.Proxy { 108 | return func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 109 | return r, err 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/juju/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | 6 | "melody/config" 7 | ) 8 | 9 | // Namespace 命名空间是用来存储和访问路由器自定义配置数据 10 | const Namespace = "melody_ratelimit_router" 11 | 12 | // Config 是包含路由器中间件参数的自定义配置结构 13 | type Config struct { 14 | MaxRate int64 15 | Strategy string 16 | ClientMaxRate int64 17 | Key string 18 | } 19 | 20 | // ZeroCfg ZeroCfg 是Config struct的零值 21 | var ZeroCfg = Config{} 22 | 23 | // ConfigGetter 实现config.ConfigGetter接口。它解析速率适配器的extra config,如果出了问题,则返回一个ZeroCfg。 24 | func ConfigGetter(e config.ExtraConfig) interface{} { 25 | v, ok := e[Namespace] 26 | if !ok { 27 | return ZeroCfg 28 | } 29 | tmp, ok := v.(map[string]interface{}) 30 | if !ok { 31 | return ZeroCfg 32 | } 33 | cfg := Config{} 34 | if v, ok := tmp["maxRate"]; ok { 35 | switch val := v.(type) { 36 | case int64: 37 | cfg.MaxRate = val 38 | case int: 39 | cfg.MaxRate = int64(val) 40 | case float64: 41 | cfg.MaxRate = int64(val) 42 | } 43 | } 44 | if v, ok := tmp["strategy"]; ok { 45 | cfg.Strategy = fmt.Sprintf("%v", v) 46 | } 47 | if v, ok := tmp["clientMaxRate"]; ok { 48 | switch val := v.(type) { 49 | case int64: 50 | cfg.ClientMaxRate = val 51 | case int: 52 | cfg.ClientMaxRate = int64(val) 53 | case float64: 54 | cfg.ClientMaxRate = int64(val) 55 | } 56 | } 57 | if v, ok := tmp["key"]; ok { 58 | cfg.Key = fmt.Sprintf("%v", v) 59 | } 60 | return cfg 61 | } 62 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/juju/router/router_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "melody/config" 8 | ) 9 | 10 | func TestConfigGetter(t *testing.T) { 11 | serializedCfg := []byte(`{ 12 | "melody_ratelimit_router": { 13 | "maxRate":10 14 | } 15 | }`) 16 | var dat config.ExtraConfig 17 | if err := json.Unmarshal(serializedCfg, &dat); err != nil { 18 | t.Error(err.Error()) 19 | } 20 | cfg := ConfigGetter(dat).(Config) 21 | if cfg.MaxRate != 10 { 22 | t.Errorf("wrong value for MaxRate. Want: 10, have: %d", cfg.MaxRate) 23 | } 24 | if cfg.ClientMaxRate != 0 { 25 | t.Errorf("wrong value for ClientMaxRate. Want: 0, have: %d", cfg.ClientMaxRate) 26 | } 27 | if cfg.Strategy != "" { 28 | t.Errorf("wrong value for Strategy. Want: '', have: %s", cfg.Strategy) 29 | } 30 | if cfg.Key != "" { 31 | t.Errorf("wrong value for Key. Want: '', have: %s", cfg.Key) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/melodyrate_test.go: -------------------------------------------------------------------------------- 1 | package melodyrate 2 | 3 | //import ( 4 | // "context" 5 | // "fmt" 6 | // "runtime" 7 | // "sync" 8 | // "testing" 9 | // "time" 10 | //) 11 | // 12 | //func TestMemoryBackend(t *testing.T) { 13 | // for _, tc := range []struct { 14 | // name string 15 | // s int 16 | // f func(context.Context, time.Duration) Backend 17 | // }{ 18 | // {name: "memory", s: 1, f: func(ctx context.Context, ttl time.Duration) Backend { return NewMemoryBackend(ctx, ttl) }}, 19 | // {name: "sharded", s: 256, f: func(ctx context.Context, ttl time.Duration) Backend { 20 | // return NewShardedMemoryBackend(ctx, 256, ttl, PseudoFNV64a) 21 | // }}, 22 | // } { 23 | // t.Run(tc.name, func(t *testing.T) { 24 | // testBackend(t, tc.s, tc.f) 25 | // }) 26 | // } 27 | //} 28 | // 29 | //func testBackend(t *testing.T, storesInit int, f func(context.Context, time.Duration) Backend) { 30 | // ttl := time.Second 31 | // ctx, cancel := context.WithCancel(context.Background()) 32 | // defer cancel() 33 | // 34 | // mb := f(ctx, ttl) 35 | // total := 1000 * runtime.NumCPU() 36 | // 37 | // <-time.After(ttl) 38 | // 39 | // wg := new(sync.WaitGroup) 40 | // 41 | // for w := 0; w < 10; w++ { 42 | // wg.Add(1) 43 | // go func() { 44 | // for i := 0; i < total; i++ { 45 | // mb.Store(fmt.Sprintf("key-%d", i), i) 46 | // } 47 | // wg.Done() 48 | // }() 49 | // } 50 | // wg.Wait() 51 | // 52 | // noResult := func() interface{} { return nil } 53 | // 54 | // for i := 0; i < total; i++ { 55 | // v := mb.Load(fmt.Sprintf("key-%d", i), noResult) 56 | // if v == nil { 57 | // t.Errorf("key %d not present", i) 58 | // return 59 | // } 60 | // if res, ok := v.(int); !ok || res != i { 61 | // t.Errorf("unexpected value. want: %d, have: %v", i, v) 62 | // return 63 | // } 64 | // } 65 | // 66 | // <-time.After(2 * ttl) 67 | // 68 | // for i := 0; i < total; i++ { 69 | // if v := mb.Load(fmt.Sprintf("key-%d", i), noResult); v != nil { 70 | // t.Errorf("key %d present after 2 TTL: %v", i, v) 71 | // return 72 | // } 73 | // } 74 | //} 75 | -------------------------------------------------------------------------------- /middleware/melody-ratelimit/pseudoFNV64a.go: -------------------------------------------------------------------------------- 1 | package melodyrate 2 | 3 | const ( 4 | offset64 uint64 = 14695981039346656037 5 | prime64 = 1099511628211 6 | ) 7 | 8 | // PseudoFNV64a 接受一个string 9 | func PseudoFNV64a(s string) uint64 { 10 | h := offset64 11 | for i := 0; i < len(s); i++ { 12 | h ^= uint64(s[i]) 13 | h *= prime64 14 | } 15 | return h 16 | } 17 | -------------------------------------------------------------------------------- /middleware/melody-rss/rss.go: -------------------------------------------------------------------------------- 1 | package rss 2 | 3 | import ( 4 | "io" 5 | "melody/encoding" 6 | 7 | "github.com/mmcdole/gofeed" 8 | ) 9 | 10 | // Name is the key for the rss encoding 11 | const Name = "rss" 12 | 13 | func Register() error { 14 | return encoding.Register(Name, DecoderFactory) 15 | } 16 | 17 | func DecoderFactory(_ bool) func(io.Reader, *map[string]interface{}) error { 18 | return NewDecoder() 19 | } 20 | 21 | // NewDecoder returns the RSS decoder 22 | func NewDecoder() func(io.Reader, *map[string]interface{}) error { 23 | fp := gofeed.NewParser() 24 | return func(r io.Reader, v *map[string]interface{}) error { 25 | feed, err := fp.Parse(r) 26 | if err != nil { 27 | return err 28 | } 29 | *(v) = map[string]interface{}{ 30 | "items": feed.Items, 31 | "author": feed.Author, 32 | "categories": feed.Categories, 33 | "custom": feed.Custom, 34 | "copyright": feed.Copyright, 35 | "description": feed.Description, 36 | "type": feed.FeedType, 37 | "language": feed.Language, 38 | "title": feed.Title, 39 | "published": feed.Published, 40 | "updated": feed.Updated, 41 | } 42 | if feed.Image != nil { 43 | (*v)["img_url"] = feed.Image.URL 44 | } 45 | return nil 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /middleware/melody-viper/viper.go: -------------------------------------------------------------------------------- 1 | package viper 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "melody/config" 6 | "reflect" 7 | "unsafe" 8 | ) 9 | 10 | //ViperParser extends viper 11 | type ViperParser struct { 12 | viper *viper.Viper 13 | } 14 | 15 | //New return new parser extends viper 16 | func New() ViperParser { 17 | return ViperParser{ 18 | viper: viper.New(), 19 | } 20 | } 21 | 22 | //实现 config 中 parser 接口 23 | func (p ViperParser) Parse(configFile string) (config.ServiceConfig, error) { 24 | p.viper.SetConfigFile(configFile) 25 | p.viper.AutomaticEnv() 26 | var cfg config.ServiceConfig 27 | 28 | if err := p.viper.ReadInConfig(); err != nil { 29 | return cfg, checkErr(err, configFile) 30 | } 31 | 32 | if err := p.viper.Unmarshal(&cfg); err != nil { 33 | return cfg, checkErr(err, configFile) 34 | } 35 | 36 | if err := cfg.Init(); err != nil { 37 | return cfg, config.CheckErr(err, configFile) 38 | } 39 | 40 | return cfg, nil 41 | } 42 | 43 | func checkErr(err error, configFile string) error { 44 | switch e := err.(type) { 45 | case viper.ConfigParseError: 46 | var subErr error 47 | rs := reflect.ValueOf(&e).Elem() 48 | rf := rs.Field(0) 49 | ri := reflect.ValueOf(&subErr).Elem() 50 | 51 | rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem() 52 | 53 | ri.Set(rf) 54 | 55 | return checkErr(subErr, configFile) 56 | default: 57 | return config.CheckErr(err, configFile) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /middleware/melody-xml/xml.go: -------------------------------------------------------------------------------- 1 | package xml 2 | 3 | import ( 4 | "github.com/clbanning/mxj" 5 | "io" 6 | "melody/encoding" 7 | ) 8 | 9 | const XML = "xml" 10 | 11 | type xmlReader struct { 12 | r io.Reader 13 | } 14 | 15 | func (x xmlReader) Read(p []byte) (n int, err error) { 16 | n, err = x.r.Read(p) 17 | 18 | if err != io.EOF { 19 | return n, err 20 | } 21 | 22 | if len(p) == n { 23 | return n, nil 24 | } 25 | 26 | p[n] = ([]byte("\n"))[0] 27 | return n + 1, err 28 | } 29 | 30 | func Register() { 31 | encoding.Register(XML, NewXMLDecoder) 32 | } 33 | func NewXMLDecoder(isCollection bool) func(io.Reader, *map[string]interface{}) error { 34 | if isCollection { 35 | return CollectionDecoder() 36 | } else { 37 | return Decoder() 38 | } 39 | } 40 | 41 | func CollectionDecoder() encoding.Decoder { 42 | return func(reader io.Reader, i *map[string]interface{}) error { 43 | mv, err := mxj.NewMapXmlReader(xmlReader{r: reader}) 44 | if err != nil { 45 | return err 46 | } 47 | *i = map[string]interface{}{"collection": mv} 48 | return nil 49 | } 50 | } 51 | 52 | func Decoder() encoding.Decoder { 53 | return func(reader io.Reader, i *map[string]interface{}) error { 54 | mp, err := mxj.NewMapXmlReader(xmlReader{r: reader}) 55 | if err != nil { 56 | return err 57 | } 58 | *i = mp 59 | return nil 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | ) 7 | 8 | // Scan ... 读取插件 9 | func Scan(folder, pattern string) ([]string, error) { 10 | files, erro := ioutil.ReadDir(folder) 11 | if erro != nil { 12 | return []string{}, erro 13 | } 14 | 15 | plugins := []string{} 16 | for _, file := range files { 17 | if !file.IsDir() && strings.Contains(file.Name(), pattern) { 18 | plugins = append(plugins, folder+file.Name()) 19 | } 20 | } 21 | 22 | return plugins, nil 23 | } 24 | -------------------------------------------------------------------------------- /proxy/balancing.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "melody/sd" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | func NewLoadBalancedMiddlewareWithSubscriber(subscriber sd.Subscriber) Middleware { 11 | return newLoadBalancedMiddleware(sd.NewBalancer(subscriber)) 12 | } 13 | 14 | func newLoadBalancedMiddleware(lb sd.Balancer) Middleware { 15 | return func(next ...Proxy) Proxy { 16 | if len(next) > 1 { 17 | panic(ErrTooManyProxies) 18 | } 19 | return func(ctx context.Context, request *Request) (*Response, error) { 20 | host, err := lb.Host() 21 | if err != nil { 22 | return nil, err 23 | } 24 | r := request.Clone() 25 | 26 | var b strings.Builder 27 | b.WriteString(host) 28 | b.WriteString(r.Path) 29 | r.URL, err = url.Parse(b.String()) 30 | if err != nil { 31 | return nil, err 32 | } 33 | if len(r.Query) > 0 { 34 | r.URL.RawQuery += "&" + r.Query.Encode() 35 | } 36 | 37 | return next[0](ctx, &r) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /proxy/concurrent.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "melody/config" 7 | "time" 8 | ) 9 | 10 | var errNullResult = errors.New("invalid response") 11 | 12 | func NewConcurrentCallMiddleware(backend *config.Backend) Middleware { 13 | // Check proxy 14 | if backend.ConcurrentCalls == 1 { 15 | panic(ErrTooManyProxies) 16 | } 17 | 18 | serviceTimeout := time.Duration(75*backend.Timeout.Nanoseconds()/100) * time.Nanosecond 19 | 20 | return func(proxy ...Proxy) Proxy { 21 | if len(proxy) > 1 { 22 | panic(ErrTooManyProxies) 23 | } 24 | 25 | return func(ctx context.Context, request *Request) (*Response, error) { 26 | totalCtx, cancel := context.WithTimeout(ctx, serviceTimeout) 27 | 28 | results := make(chan *Response, backend.ConcurrentCalls) 29 | failed := make(chan error, backend.ConcurrentCalls) 30 | 31 | for i := 0; i < backend.ConcurrentCalls; i++ { 32 | go processConcurrentCall(totalCtx, proxy[0], request, results, failed) 33 | } 34 | 35 | var response *Response 36 | var err error 37 | 38 | for i := 0; i < backend.ConcurrentCalls; i++ { 39 | select { 40 | case response = <-results: 41 | if response != nil && response.IsComplete { 42 | cancel() 43 | return response, nil 44 | } 45 | case err = <-failed: 46 | case <-ctx.Done(): 47 | } 48 | } 49 | cancel() 50 | return response, err 51 | } 52 | } 53 | } 54 | 55 | func processConcurrentCall(ctx context.Context, proxy Proxy, request *Request, responses chan *Response, errors chan error) { 56 | localCtx, cancel := context.WithCancel(ctx) 57 | 58 | resp, err := proxy(localCtx, request) 59 | 60 | if err != nil { 61 | errors <- err 62 | cancel() 63 | return 64 | } 65 | 66 | if resp == nil { 67 | errors <- errNullResult 68 | cancel() 69 | return 70 | } 71 | 72 | select { 73 | case responses <- resp: 74 | case <-ctx.Done(): 75 | errors <- ctx.Err() 76 | } 77 | cancel() 78 | } 79 | -------------------------------------------------------------------------------- /proxy/factory.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "melody/config" 5 | "melody/logging" 6 | "melody/sd" 7 | ) 8 | 9 | // Factory 作为ProxyFactory的标准 10 | type Factory interface { 11 | New(cfg *config.EndpointConfig) (Proxy, error) 12 | } 13 | 14 | type FactoryFunc func(*config.EndpointConfig) (Proxy, error) 15 | 16 | // New implements the Factory interface 17 | func (f FactoryFunc) New(cfg *config.EndpointConfig) (Proxy, error) { return f(cfg) } 18 | 19 | type defaultFactory struct { 20 | backendFactory BackendFactory 21 | logger logging.Logger 22 | subscriberFactory sd.SubscriberFactory 23 | } 24 | 25 | func NewDefaultFactory(factory BackendFactory, logger logging.Logger) Factory { 26 | return NewDefaultFactoryWithSubscriberFactory(factory, logger, sd.GetSubscriber) 27 | } 28 | 29 | func NewDefaultFactoryWithSubscriberFactory(factory BackendFactory, logger logging.Logger, subscriberFactory sd.SubscriberFactory) Factory { 30 | return defaultFactory{ 31 | backendFactory: factory, 32 | logger: logger, 33 | subscriberFactory: subscriberFactory, 34 | } 35 | } 36 | 37 | func (d defaultFactory) New(cfg *config.EndpointConfig) (p Proxy, err error) { 38 | switch len(cfg.Backends) { 39 | case 0: 40 | err = ErrNoBackends 41 | case 1: 42 | p, err = d.NewSingle(cfg) 43 | default: 44 | p, err = d.NewMulti(cfg) 45 | } 46 | 47 | if err != nil { 48 | return 49 | } 50 | // 执行顺序:⑥ 51 | p = NewStaticDataMiddleware(cfg)(p) 52 | return 53 | } 54 | 55 | func (d defaultFactory) NewStack(backend *config.Backend) (p Proxy) { 56 | // 根据config.Backend定制backendProxy 执行顺序:④ 57 | p = d.backendFactory(backend) 58 | // 均衡中间件注册(在此处调用对应的服务发现) 执行顺序:③ 59 | p = NewLoadBalancedMiddlewareWithSubscriber(d.subscriberFactory(backend))(p) 60 | if backend.ConcurrentCalls > 1 { 61 | // 并发调用 > 1 执行顺序:② 62 | p = NewConcurrentCallMiddleware(backend)(p) 63 | } 64 | // 基础的Request构造器 执行顺序:① 65 | p = NewRequestBuilderMiddleware(backend)(p) 66 | return 67 | } 68 | 69 | func (d defaultFactory) NewSingle(endpointConfig *config.EndpointConfig) (Proxy, error) { 70 | return d.NewStack(endpointConfig.Backends[0]), nil 71 | } 72 | 73 | func (d defaultFactory) NewMulti(endpointConfig *config.EndpointConfig) (p Proxy, err error) { 74 | 75 | backendProxies := make([]Proxy, len(endpointConfig.Backends)) 76 | for i, v := range endpointConfig.Backends { 77 | backendProxies[i] = d.NewStack(v) 78 | } 79 | // 执行顺序:⑤ 80 | p = NewMergeDataMiddleware(endpointConfig)(backendProxies...) 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /proxy/http_response.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "melody/encoding" 6 | "net/http" 7 | ) 8 | 9 | // HTTPResponseParserConfig 封装了解码器和格式化器,作用于proxy.Response 10 | type HTTPResponseParserConfig struct { 11 | Decoder encoding.Decoder 12 | EntityFormatter EntityFormatter 13 | } 14 | 15 | // NoOpHTTPResponseParser 不对http.Response做任何format,直接封装成proxy.Response 16 | func NoOpHTTPResponseParser(ctx context.Context, resp *http.Response) (*Response, error) { 17 | return &Response{ 18 | Data: map[string]interface{}{}, 19 | IsComplete: true, 20 | Io: NewReadCloserWrapper(ctx, resp.Body), 21 | Metadata: Metadata{ 22 | Headers: resp.Header, 23 | StatusCode: resp.StatusCode, 24 | }, 25 | }, nil 26 | } 27 | 28 | func DefaultHTTPResponseParserFactory(cfg HTTPResponseParserConfig) HTTPResponseParser { 29 | return func(ctx context.Context, resp *http.Response) (*Response, error) { 30 | defer resp.Body.Close() 31 | 32 | var data map[string]interface{} 33 | err := cfg.Decoder(resp.Body, &data) 34 | if err != nil { 35 | return nil, err 36 | } 37 | response := Response{ 38 | Data: data, 39 | IsComplete: true, 40 | } 41 | response = cfg.EntityFormatter.Format(response) 42 | return &response, nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /proxy/http_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "melody/config" 8 | "melody/encoding" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestNewHTTPProxy_ok(t *testing.T) { 17 | expectedMethod := "GET" 18 | backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | if r.ContentLength != 11 { 20 | t.Errorf("unexpected request size. Want: 11. Have: %d", r.ContentLength) 21 | } 22 | if h := r.Header.Get("Content-Length"); h != "11" { 23 | t.Errorf("unexpected content-length header. Want: 11. Have: %s", h) 24 | } 25 | if r.Method != expectedMethod { 26 | t.Errorf("Wrong request method. Want: %s. Have: %s", expectedMethod, r.Method) 27 | } 28 | if h := r.Header.Get("X-First"); h != "first" { 29 | t.Errorf("unexpected first header: %s", h) 30 | } 31 | if h := r.Header.Get("X-Second"); h != "second" { 32 | t.Errorf("unexpected second header: %s", h) 33 | } 34 | r.Header.Del("X-Second") 35 | fmt.Fprintf(w, "{\"supu\":42, \"tupu\":true, \"foo\": \"bar\"}") 36 | })) 37 | defer backendServer.Close() 38 | 39 | rpURL, _ := url.Parse(backendServer.URL) 40 | backend := config.Backend{ 41 | Decoder: encoding.JSONDecoder(), 42 | } 43 | request := Request{ 44 | Method: expectedMethod, 45 | Path: "/", 46 | URL: rpURL, 47 | Body: newDummyReadCloser(`{"abc": 42}`), 48 | Headers: map[string][]string{ 49 | "X-First": {"first"}, 50 | "X-Second": {"second"}, 51 | "Content-Length": {"11"}, 52 | }, 53 | } 54 | mustEnd := time.After(time.Duration(150) * time.Millisecond) 55 | 56 | result, err := HTTPProxyFactory(http.DefaultClient)(&backend)(context.Background(), &request) 57 | if err != nil { 58 | t.Errorf("The proxy returned an unexpected error: %s\n", err.Error()) 59 | return 60 | } 61 | if result == nil { 62 | t.Errorf("The proxy returned a null result\n") 63 | return 64 | } 65 | select { 66 | case <-mustEnd: 67 | t.Errorf("Error: expected response") 68 | return 69 | default: 70 | } 71 | 72 | tmp, ok := result.Data["supu"] 73 | if !ok { 74 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 75 | } 76 | supuValue, err := tmp.(json.Number).Int64() 77 | if err != nil || supuValue != 42 { 78 | t.Errorf("The proxy returned an unexpected result: %v\n", supuValue) 79 | } 80 | if v, ok := result.Data["tupu"]; !ok || !v.(bool) { 81 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 82 | } 83 | if v, ok := result.Data["foo"]; !ok || v.(string) != "bar" { 84 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 85 | } 86 | if v, ok := request.Headers["X-Second"]; !ok || len(v) != 1 { 87 | t.Errorf("the proxy request headers were changed: %v", request.Headers) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "melody/config" 8 | ) 9 | 10 | // Namespace to be used in extra config 11 | const Namespace = "melody_proxy" 12 | 13 | var ( 14 | // ErrNoBackends is the error returned when an endpoint has no backends defined 15 | ErrNoBackends = errors.New("all endpoints must have at least one backend") 16 | // ErrTooManyBackends is the error returned when an endpoint has too many backends defined 17 | ErrTooManyBackends = errors.New("too many backends for this proxy") 18 | // ErrTooManyProxies is the error returned when a middleware has too many proxies defined 19 | ErrTooManyProxies = errors.New("too many proxies for this proxy middleware") 20 | // ErrNotEnoughProxies is the error returned when an endpoint has not enough proxies defined 21 | ErrNotEnoughProxies = errors.New("not enough proxies for this endpoint") 22 | ) 23 | 24 | // Proxy 定义了代理 25 | // 输入: context, request 26 | // 出入: response, error 27 | type Proxy func(context.Context, *Request) (*Response, error) 28 | 29 | type BackendFactory func(backend *config.Backend) Proxy 30 | 31 | // Metadata 包含了response header 和 response code 32 | type Metadata struct { 33 | Headers map[string][]string 34 | StatusCode int 35 | } 36 | 37 | // Response 作为proxy的输出 38 | type Response struct { 39 | Data map[string]interface{} 40 | IsComplete bool 41 | Io io.Reader 42 | Metadata Metadata 43 | } 44 | 45 | type Middleware func(...Proxy) Proxy 46 | 47 | func EmptyMiddleware(next ...Proxy) Proxy { 48 | if len(next) > 1 { 49 | panic(ErrTooManyProxies) 50 | } 51 | return next[0] 52 | } 53 | 54 | type readCloserWrapper struct { 55 | ctx context.Context 56 | rc io.ReadCloser 57 | } 58 | 59 | func (r readCloserWrapper) Read(p []byte) (n int, err error) { 60 | return r.rc.Read(p) 61 | } 62 | 63 | func (r readCloserWrapper) closeWhenCancel() { 64 | <-r.ctx.Done() 65 | r.rc.Close() 66 | } 67 | 68 | func NewReadCloserWrapper(ctx context.Context, reader io.ReadCloser) io.Reader { 69 | wrapper := readCloserWrapper{ 70 | ctx: ctx, 71 | rc: reader, 72 | } 73 | go wrapper.closeWhenCancel() 74 | return wrapper 75 | } 76 | 77 | func NoopProxy(_ context.Context, _ *Request) (*Response, error) { return nil, nil } 78 | -------------------------------------------------------------------------------- /proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func newDummyReadCloser(content string) io.ReadCloser { 12 | return dummyReadCloser{strings.NewReader(content)} 13 | } 14 | 15 | type dummyReadCloser struct { 16 | reader io.Reader 17 | } 18 | 19 | func (d dummyReadCloser) Read(p []byte) (int, error) { 20 | return d.reader.Read(p) 21 | } 22 | 23 | func (d dummyReadCloser) Close() error { 24 | return nil 25 | } 26 | 27 | func explosiveProxy(t *testing.T) Proxy { 28 | return func(ctx context.Context, _ *Request) (*Response, error) { 29 | t.Error("This proxy shouldn't been executed!") 30 | return &Response{}, nil 31 | } 32 | } 33 | 34 | func dummyProxy(r *Response) Proxy { 35 | return func(_ context.Context, _ *Request) (*Response, error) { 36 | return r, nil 37 | } 38 | } 39 | 40 | func delayedProxy(t *testing.T, timeout time.Duration, r *Response) Proxy { 41 | return func(ctx context.Context, _ *Request) (*Response, error) { 42 | select { 43 | case <-ctx.Done(): 44 | return nil, ctx.Err() 45 | case <-time.After(timeout): 46 | return r, nil 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /proxy/register.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import "melody/register" 4 | 5 | type Register struct { 6 | *combinerRegister 7 | } 8 | 9 | type combinerRegister struct { 10 | data register.Untyped 11 | fallback ResponseCombiner 12 | } 13 | 14 | func NewRegister() *Register { 15 | return &Register{ 16 | responseCombiners, 17 | } 18 | } 19 | 20 | func newCombinerRegister(data map[string]ResponseCombiner, fallback ResponseCombiner) *combinerRegister { 21 | r := register.New() 22 | for k, v := range data { 23 | r.Register(k, v) 24 | } 25 | return &combinerRegister{r, fallback} 26 | } 27 | 28 | func (r *combinerRegister) GetResponseCombiner(name string) (ResponseCombiner, bool) { 29 | v, ok := r.data.Get(name) 30 | if !ok { 31 | return r.fallback, ok 32 | } 33 | if rc, ok := v.(ResponseCombiner); ok { 34 | return rc, ok 35 | } 36 | return r.fallback, ok 37 | } 38 | 39 | func (r *combinerRegister) SetResponseCombiner(name string, rc ResponseCombiner) { 40 | r.data.Register(name, rc) 41 | } 42 | -------------------------------------------------------------------------------- /proxy/register_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewRegister_responseCombiner_ok(t *testing.T) { 8 | r := NewRegister() 9 | r.SetResponseCombiner("name1", func(total int, parts []*Response) *Response { 10 | if total < 0 || total >= len(parts) { 11 | return nil 12 | } 13 | return parts[total] 14 | }) 15 | 16 | rc, ok := r.GetResponseCombiner("name1") 17 | if !ok { 18 | t.Error("expecting response combiner") 19 | return 20 | } 21 | 22 | result := rc(0, []*Response{{IsComplete: true, Data: map[string]interface{}{"a": 42}}}) 23 | 24 | if result == nil { 25 | t.Error("expecting result") 26 | return 27 | } 28 | 29 | if !result.IsComplete { 30 | t.Error("expecting a complete result") 31 | return 32 | } 33 | 34 | if len(result.Data) != 1 { 35 | t.Error("unexpected result size:", len(result.Data)) 36 | return 37 | } 38 | } 39 | 40 | func TestNewRegister_responseCombiner_fallbackIfErrored(t *testing.T) { 41 | r := NewRegister() 42 | 43 | r.data.Register("errored", true) 44 | 45 | rc, ok := r.GetResponseCombiner("errored") 46 | if !ok { 47 | t.Error("expecting response combiner") 48 | return 49 | } 50 | 51 | original := &Response{IsComplete: true, Data: map[string]interface{}{"a": 42}} 52 | 53 | result := rc(0, []*Response{original}) 54 | 55 | if result != original { 56 | t.Error("unexpected result:", result) 57 | return 58 | } 59 | } 60 | 61 | func TestNewRegister_responseCombiner_fallbackIfUnknown(t *testing.T) { 62 | r := NewRegister() 63 | 64 | rc, ok := r.GetResponseCombiner("unknown") 65 | if ok { 66 | t.Error("the response combiner should not be found") 67 | return 68 | } 69 | 70 | original := &Response{IsComplete: true, Data: map[string]interface{}{"a": 42}} 71 | 72 | result := rc(0, []*Response{original}) 73 | 74 | if result != original { 75 | t.Error("unexpected result:", result) 76 | return 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /proxy/request.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "net/url" 8 | ) 9 | 10 | // Request 包含了从Endpoint发送 11 | // 到backends的数据 12 | type Request struct { 13 | Method string 14 | URL *url.URL 15 | Query url.Values 16 | Path string 17 | Body io.ReadCloser 18 | Params map[string]string 19 | Headers map[string][]string 20 | } 21 | 22 | func (r *Request) Clone() Request { 23 | return Request{ 24 | Method: r.Method, 25 | URL: r.URL, 26 | Query: r.Query, 27 | Path: r.Path, 28 | Body: r.Body, 29 | Params: r.Params, 30 | Headers: r.Headers, 31 | } 32 | } 33 | 34 | // CloneRequest 返回一个请求副本 35 | func CloneRequest(r *Request) *Request { 36 | clone := r.Clone() 37 | clone.Headers = CloneRequestHeaders(r.Headers) 38 | clone.Params = CloneRequestParams(r.Params) 39 | if r.Body == nil { 40 | return &clone 41 | } 42 | buf := new(bytes.Buffer) 43 | buf.ReadFrom(r.Body) 44 | r.Body.Close() 45 | 46 | r.Body = ioutil.NopCloser(bytes.NewReader(buf.Bytes())) 47 | clone.Body = ioutil.NopCloser(buf) 48 | 49 | return &clone 50 | } 51 | 52 | // CloneRequestHeaders 返回一个接收到的请求头的副本 53 | func CloneRequestHeaders(headers map[string][]string) map[string][]string { 54 | m := make(map[string][]string, len(headers)) 55 | for k, vs := range headers { 56 | tmp := make([]string, len(vs)) 57 | copy(tmp, vs) 58 | m[k] = tmp 59 | } 60 | return m 61 | } 62 | 63 | // CloneRequestParams 返回接收到的请求参数的副本 64 | func CloneRequestParams(params map[string]string) map[string]string { 65 | m := make(map[string]string, len(params)) 66 | for k, v := range params { 67 | m[k] = v 68 | } 69 | return m 70 | } 71 | 72 | func (r *Request) GeneratePath(URLPattern string) { 73 | if len(r.Params) == 0 { 74 | r.Path = URLPattern 75 | return 76 | } 77 | buff := []byte(URLPattern) 78 | for k, v := range r.Params { 79 | key := []byte{} 80 | key = append(key, "{{."...) 81 | key = append(key, k...) 82 | key = append(key, "}}"...) 83 | buff = bytes.Replace(buff, key, []byte(v), -1) 84 | } 85 | r.Path = string(buff) 86 | } 87 | -------------------------------------------------------------------------------- /proxy/shadow.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "melody/config" 6 | ) 7 | 8 | const ( 9 | shadowKey = "shadow" 10 | ) 11 | 12 | type shadowFactory struct { 13 | f Factory 14 | } 15 | 16 | // New check the Backends for an ExtraConfig with the "shadow" param to true 17 | // 实现工厂接口 18 | func (s shadowFactory) New(cfg *config.EndpointConfig) (p Proxy, err error) { 19 | if len(cfg.Backends) == 0 { 20 | err = ErrNoBackends 21 | return 22 | } 23 | 24 | shadow := []*config.Backend{} 25 | regular := []*config.Backend{} 26 | 27 | for _, b := range cfg.Backends { 28 | if isShadowBackend(b) { 29 | shadow = append(shadow, b) 30 | continue 31 | } 32 | regular = append(regular, b) 33 | } 34 | 35 | cfg.Backends = regular 36 | 37 | p, err = s.f.New(cfg) 38 | 39 | if len(shadow) > 0 { 40 | cfg.Backends = shadow 41 | pShadow, _ := s.f.New(cfg) 42 | p = ShadowMiddleware(p, pShadow) 43 | } 44 | 45 | return 46 | } 47 | 48 | // NewShadowFactory 使用提供的工厂创建一个新的shadowFactory 49 | func NewShadowFactory(f Factory) Factory { 50 | return shadowFactory{f} 51 | } 52 | 53 | // ShadowMiddleware 是一个创建shadowProxy的中间件 54 | func ShadowMiddleware(next ...Proxy) Proxy { 55 | switch len(next) { 56 | case 0: 57 | panic(ErrNotEnoughProxies) 58 | case 1: 59 | return next[0] 60 | case 2: 61 | return NewShadowProxy(next[0], next[1]) 62 | default: 63 | panic(ErrTooManyProxies) 64 | } 65 | } 66 | 67 | // NewShadowProxy 返回一个向p1和p2发送请求但忽略p2响应的代理 68 | func NewShadowProxy(p1, p2 Proxy) Proxy { 69 | return func(ctx context.Context, request *Request) (*Response, error) { 70 | go p2(newcontextWrapper(ctx), CloneRequest(request)) 71 | return p1(ctx, request) 72 | } 73 | } 74 | 75 | func isShadowBackend(c *config.Backend) bool { 76 | if v, ok := c.ExtraConfig[Namespace]; ok { 77 | if e, ok := v.(map[string]interface{}); ok { 78 | if v, ok := e[shadowKey]; ok { 79 | c, ok := v.(bool) 80 | return ok && c 81 | } 82 | } 83 | } 84 | return false 85 | } 86 | 87 | type contextWrapper struct { 88 | context.Context 89 | data context.Context 90 | } 91 | 92 | func (c contextWrapper) Value(key interface{}) interface{} { 93 | return c.data.Value(key) 94 | } 95 | 96 | func newcontextWrapper(data context.Context) contextWrapper { 97 | return contextWrapper{ 98 | Context: context.Background(), 99 | data: data, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /proxy/static.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "melody/config" 6 | ) 7 | 8 | const ( 9 | staticKey = "static" 10 | 11 | staticAlwaysStrategy = "always" 12 | staticIfSuccessStrategy = "success" 13 | staticIfErroredStrategy = "errored" 14 | staticIfCompleteStrategy = "complete" 15 | staticIfIncompleteStrategy = "incomplete" 16 | ) 17 | 18 | type staticConfig struct { 19 | Data map[string]interface{} 20 | Strategy string 21 | Match func(*Response, error) bool 22 | } 23 | 24 | // NewStaticDataMiddleware 定义了插入静态数据中间件 25 | func NewStaticDataMiddleware(endpoint *config.EndpointConfig) Middleware { 26 | v, ok := getStaticConfig(endpoint.ExtraConfig) 27 | if !ok { 28 | return EmptyMiddleware 29 | } 30 | 31 | return func(proxy ...Proxy) Proxy { 32 | if len(proxy) > 1 { 33 | panic(ErrTooManyProxies) 34 | } 35 | return func(ctx context.Context, request *Request) (response *Response, e error) { 36 | result, err := proxy[0](ctx, request) 37 | if !v.Match(result, err) { 38 | return result, err 39 | } 40 | 41 | if result == nil { 42 | result = &Response{Data: map[string]interface{}{}} 43 | } 44 | 45 | for k, v := range v.Data { 46 | result.Data[k] = v 47 | } 48 | 49 | return result, err 50 | } 51 | } 52 | } 53 | 54 | func getStaticConfig(extraConfig config.ExtraConfig) (staticConfig, bool) { 55 | v, ok := extraConfig[Namespace] 56 | if !ok { 57 | return staticConfig{}, ok 58 | } 59 | e, ok := v.(map[string]interface{}) 60 | if !ok { 61 | return staticConfig{}, ok 62 | } 63 | v, ok = e[staticKey] 64 | if !ok { 65 | return staticConfig{}, ok 66 | } 67 | tmp, ok := v.(map[string]interface{}) 68 | if !ok { 69 | return staticConfig{}, ok 70 | } 71 | data, ok := tmp["data"].(map[string]interface{}) 72 | if !ok { 73 | return staticConfig{}, ok 74 | } 75 | 76 | name, ok := tmp["strategy"].(string) 77 | if !ok { 78 | name = staticAlwaysStrategy 79 | } 80 | cfg := staticConfig{ 81 | Data: data, 82 | Strategy: name, 83 | Match: staticAlwaysMatch, 84 | } 85 | switch name { 86 | case staticAlwaysStrategy: 87 | cfg.Match = staticAlwaysMatch 88 | case staticIfSuccessStrategy: 89 | cfg.Match = staticIfSuccessMatch 90 | case staticIfErroredStrategy: 91 | cfg.Match = staticIfErroredMatch 92 | case staticIfCompleteStrategy: 93 | cfg.Match = staticIfCompleteMatch 94 | case staticIfIncompleteStrategy: 95 | cfg.Match = staticIfIncompleteMatch 96 | } 97 | return cfg, true 98 | } 99 | 100 | func staticAlwaysMatch(_ *Response, _ error) bool { return true } 101 | func staticIfSuccessMatch(_ *Response, err error) bool { return err == nil } 102 | func staticIfErroredMatch(_ *Response, err error) bool { return err != nil } 103 | func staticIfCompleteMatch(r *Response, err error) bool { 104 | return err == nil && r != nil && r.IsComplete 105 | } 106 | func staticIfIncompleteMatch(r *Response, _ error) bool { return r == nil || !r.IsComplete } 107 | -------------------------------------------------------------------------------- /register/internal/untyped.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "sync" 4 | 5 | func New() *Untyped { 6 | return &Untyped{ 7 | data: map[string]interface{}{}, 8 | mutex: &sync.RWMutex{}, 9 | } 10 | } 11 | 12 | type Untyped struct { 13 | data map[string]interface{} 14 | mutex *sync.RWMutex 15 | } 16 | 17 | func (u *Untyped) Register(name string, v interface{}) { 18 | u.mutex.Lock() 19 | u.data[name] = v 20 | u.mutex.Unlock() 21 | } 22 | 23 | func (u *Untyped) Get(name string) (interface{}, bool) { 24 | u.mutex.RLock() 25 | v, ok := u.data[name] 26 | u.mutex.RUnlock() 27 | return v, ok 28 | } 29 | 30 | func (u *Untyped) Clone() map[string]interface{} { 31 | u.mutex.RLock() 32 | clone := make(map[string]interface{}, len(u.data)) 33 | for k, v := range u.data { 34 | clone[k] = v 35 | } 36 | u.mutex.RUnlock() 37 | return clone 38 | } 39 | -------------------------------------------------------------------------------- /register/register.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import "melody/register/internal" 4 | 5 | type Untyped interface { 6 | Register(name string, v interface{}) 7 | Get(name string) (interface{}, bool) 8 | Clone() map[string]interface{} 9 | } 10 | 11 | func New() Untyped { 12 | return internal.New() 13 | } 14 | 15 | func NewSpaces() *Namespaced { 16 | return &Namespaced{New()} 17 | } 18 | 19 | type Namespaced struct { 20 | data Untyped 21 | } 22 | 23 | func (n *Namespaced) Get(namespace string) (Untyped, bool) { 24 | v, ok := n.data.Get(namespace) 25 | if !ok { 26 | return nil, ok 27 | } 28 | register, ok := v.(Untyped) 29 | return register, ok 30 | } 31 | 32 | func (n *Namespaced) Register(namespace, name string, v interface{}) { 33 | if register, ok := n.Get(namespace); ok { 34 | register.Register(name, v) 35 | return 36 | } 37 | 38 | register := New() 39 | register.Register(name, v) 40 | n.data.Register(namespace, register) 41 | } 42 | 43 | func (n *Namespaced) AddNamespace(namespace string) { 44 | if _, ok := n.Get(namespace); ok { 45 | return 46 | } 47 | n.data.Register(namespace, New()) 48 | } 49 | -------------------------------------------------------------------------------- /register/register_test.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import "testing" 4 | 5 | func Test(t *testing.T) { 6 | r := NewSpaces() 7 | r.Register("namespace1", "name1", 42) 8 | r.AddNamespace("namespace1") 9 | r.AddNamespace("namespace2") 10 | r.Register("namespace2", "name2", true) 11 | 12 | nr, ok := r.Get("namespace1") 13 | if !ok { 14 | t.Error("namespace1 not found") 15 | return 16 | } 17 | if _, ok := nr.Get("name2"); ok { 18 | t.Error("name2 found into namespace1") 19 | return 20 | } 21 | v1, ok := nr.Get("name1") 22 | if !ok { 23 | t.Error("name1 not found") 24 | return 25 | } 26 | if i, ok := v1.(int); !ok || i != 42 { 27 | t.Error("unexpected value:", v1) 28 | } 29 | 30 | nr, ok = r.Get("namespace2") 31 | if !ok { 32 | t.Error("namespace2 not found") 33 | return 34 | } 35 | if _, ok := nr.Get("name1"); ok { 36 | t.Error("name1 found into namespace2") 37 | return 38 | } 39 | v2, ok := nr.Get("name2") 40 | if !ok { 41 | t.Error("name2 not found") 42 | return 43 | } 44 | if b, ok := v2.(bool); !ok || !b { 45 | t.Error("unexpected value:", v2) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /router/gin/debug.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "melody/logging" 7 | ) 8 | 9 | // DebugHandler creates a dummy handler function, useful for quick integration tests 10 | func DebugHandler(logger logging.Logger) gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | c.JSON(200, gin.H{ 13 | "message": "pong", 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "melody/config" 6 | http "melody/transport/http/server" 7 | ) 8 | 9 | const ( 10 | HeaderCompleteResponseValue = http.HeaderCompleteResponseValue 11 | HeaderInCompleteResponseValue = http.HeaderIncompleteResponseValue 12 | HeaderCompleteKey = http.HeaderCompleteKey 13 | ) 14 | 15 | var ( 16 | // DefaultRunServer default run server func 17 | DefaultRunServer = http.RunServer 18 | // PassHeaders 默认放行的请求头 19 | PassHeaders = http.HeadersToSend 20 | // UserAgentHeaderValue 代理的请求头标签值 21 | UserAgentHeaderValue = http.UserAgentHeaderValue 22 | ErrorInternalError = http.ErrorInternalError 23 | DefaultToHTTPError = http.DefaultToHTTPError 24 | InitHTTPDefaultTransport = http.InitHTTPDefaultTransport 25 | ) 26 | 27 | // Router 暴露出去的接口 28 | type Router interface { 29 | Run(config.ServiceConfig) 30 | } 31 | 32 | // ToHTTPError change error -> http status code 33 | type ToHTTPError http.ToHTTPError 34 | 35 | // Factory 暴露出去的接口 36 | type Factory interface { 37 | New() Router 38 | NewWithContext(context.Context) Router 39 | } 40 | -------------------------------------------------------------------------------- /sd/dnssrv/subscriber.go: -------------------------------------------------------------------------------- 1 | // Package dnssrv defines some implementations for a dns based service discovery 2 | package dnssrv 3 | 4 | import ( 5 | "fmt" 6 | "melody/config" 7 | "melody/sd" 8 | "net" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // Namespace is the key for the dns sd module 14 | const Namespace = "dns" 15 | 16 | // Register registers the dns sd subscriber factory 17 | func Register() error { 18 | return sd.RegisterSubscriberFactory(Namespace, SubscriberFactory) 19 | } 20 | 21 | var ( 22 | // TTL is the duration of the cached data 23 | TTL = 30 * time.Second 24 | // DefaultLookup id the function for the DNS resolution 25 | DefaultLookup = net.LookupSRV 26 | ) 27 | 28 | // SubscriberFactory builds a DNS_SRV Subscriber with the received config 29 | func SubscriberFactory(cfg *config.Backend) sd.Subscriber { 30 | return New(cfg.Host[0]) 31 | } 32 | 33 | // New creates a DNS subscriber with the default values 34 | func New(name string) sd.Subscriber { 35 | return NewDetailed(name, DefaultLookup, TTL) 36 | } 37 | 38 | // NewDetailed creates a DNS subscriber with the received values 39 | func NewDetailed(name string, lookup lookup, ttl time.Duration) sd.Subscriber { 40 | s := subscriber{name, &sd.FixedSubscriber{}, &sync.Mutex{}, ttl, lookup} 41 | s.update() 42 | go s.loop() 43 | return s 44 | } 45 | 46 | type lookup func(service, proto, name string) (cname string, addrs []*net.SRV, err error) 47 | 48 | type subscriber struct { 49 | name string 50 | cache *sd.FixedSubscriber 51 | mutex *sync.Mutex 52 | ttl time.Duration 53 | lookup lookup 54 | } 55 | 56 | // Hosts implements the subscriber interface 57 | func (s subscriber) Hosts() ([]string, error) { 58 | s.mutex.Lock() 59 | defer s.mutex.Unlock() 60 | return s.cache.Hosts() 61 | } 62 | 63 | func (s subscriber) loop() { 64 | for { 65 | <-time.After(s.ttl) 66 | s.update() 67 | } 68 | } 69 | 70 | func (s subscriber) update() { 71 | instances, err := s.resolve() 72 | if err != nil { 73 | return 74 | } 75 | s.mutex.Lock() 76 | *(s.cache) = sd.FixedSubscriber(instances) 77 | s.mutex.Unlock() 78 | } 79 | 80 | func (s subscriber) resolve() ([]string, error) { 81 | _, addrs, err := s.lookup("", "", s.name) 82 | if err != nil { 83 | return []string{}, err 84 | } 85 | instances := make([]string, len(addrs)) 86 | for i, addr := range addrs { 87 | instances[i] = fmt.Sprintf("http://%s", net.JoinHostPort(addr.Target, fmt.Sprint(addr.Port))) 88 | } 89 | return instances, nil 90 | } 91 | -------------------------------------------------------------------------------- /sd/dnssrv/subscriber_test.go: -------------------------------------------------------------------------------- 1 | package dnssrv 2 | 3 | import ( 4 | "errors" 5 | "melody/config" 6 | "melody/sd" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestSubscriber_New(t *testing.T) { 13 | if err := Register(); err != nil { 14 | t.Error("registering the dns module:", err.Error()) 15 | } 16 | srvSet := []*net.SRV{ 17 | { 18 | Port: 80, 19 | Target: "127.0.0.1", 20 | }, 21 | { 22 | Port: 81, 23 | Target: "127.0.0.1", 24 | }, 25 | } 26 | DefaultLookup = func(service, proto, name string) (cname string, addrs []*net.SRV, err error) { 27 | return "cname", srvSet, nil 28 | } 29 | 30 | s := sd.GetSubscriber(&config.Backend{Host: []string{"www.baidu.com"}, SD: Namespace}) 31 | hosts, err := s.Hosts() 32 | 33 | if err != nil { 34 | t.Error("Getting the hosts:", err.Error()) 35 | } 36 | if len(hosts) != 2 { 37 | t.Error("Wrong number of hosts:", len(hosts)) 38 | } 39 | if hosts[0] != "http://127.0.0.1:80" { 40 | t.Error("Wrong host #0 (expected http://127.0.0.1:80):", hosts[0]) 41 | } 42 | if hosts[1] != "http://127.0.0.1:81" { 43 | t.Error("Wrong host #1 (expected http://127.0.0.1:81):", hosts[1]) 44 | } 45 | } 46 | 47 | func TestSubscriber_LoockupError(t *testing.T) { 48 | errToReturn := errors.New("Some random error") 49 | defaultLookup := func(service, proto, name string) (cname string, addrs []*net.SRV, err error) { 50 | return "cname", []*net.SRV{}, errToReturn 51 | } 52 | ttl := 1 * time.Millisecond 53 | s := NewDetailed("some.example.tld", defaultLookup, ttl) 54 | hosts, err := s.Hosts() 55 | if err != nil { 56 | t.Error("Unexpected error!", err) 57 | } 58 | if len(hosts) != 0 { 59 | t.Error("Wrong number of hosts:", len(hosts)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sd/loadbalancing.go: -------------------------------------------------------------------------------- 1 | package sd 2 | 3 | import ( 4 | "errors" 5 | "runtime" 6 | "sync/atomic" 7 | 8 | "github.com/valyala/fastrand" 9 | ) 10 | 11 | // Balancer 用一个负载均衡策略去选择使用哪一个backend 12 | type Balancer interface { 13 | Host() (string, error) 14 | } 15 | 16 | var ErrNoHosts = errors.New("no hosts available") 17 | 18 | // NewBalancer 按照可用 processors 的数量来返回最合适的 balancer 19 | // If GOMAXPROCS = 1, 返回一个轮询LB,因为原子计数器没有竞争 20 | // If GOMAXPROCS > 1, 返回基于CPU数量而优化的伪随机LB 21 | func NewBalancer(subscriber Subscriber) Balancer { 22 | if p := runtime.GOMAXPROCS(-1); p == 1 { // runtime.GOMAXPROCS 返回可同时执行的最大CPU数 23 | return NewRoundRobinLB(subscriber) 24 | } 25 | return NewRandomLB(subscriber) 26 | } 27 | 28 | // NewRoundRobinLB returns a new balancer using a round robin strategy 29 | func NewRoundRobinLB(subscriber Subscriber) Balancer { 30 | if s, ok := subscriber.(FixedSubscriber); ok && len(s) == 1 { 31 | return nopBalancer(s[0]) 32 | } 33 | return &roundRobinLB{ 34 | balancer: balancer{subscriber: subscriber}, 35 | counter: 0, 36 | } 37 | } 38 | 39 | type roundRobinLB struct { 40 | balancer 41 | counter uint64 42 | } 43 | 44 | // Host implements the balancer interface 45 | func (r *roundRobinLB) Host() (string, error) { 46 | hosts, err := r.hosts() 47 | if err != nil { 48 | return "", err 49 | } 50 | // atomic.AddUint64 原子性的将val的值添加到*addr并返回新值 51 | offset := (atomic.AddUint64(&r.counter, 1) - 1) % uint64(len(hosts)) 52 | return hosts[offset], nil 53 | } 54 | 55 | // NewRandomLB 使用 fastrand 伪随机数生成器 56 | func NewRandomLB(subscriber Subscriber) Balancer { 57 | if s, ok := subscriber.(FixedSubscriber); ok && len(s) == 1 { 58 | return nopBalancer(s[0]) 59 | } 60 | return &randomLB{ 61 | balancer: balancer{subscriber: subscriber}, 62 | rand: fastrand.Uint32n, 63 | } 64 | } 65 | 66 | type randomLB struct { 67 | balancer 68 | rand func(uint32) uint32 69 | } 70 | 71 | // Host implements the balancer interface 72 | func (r *randomLB) Host() (string, error) { 73 | hosts, err := r.hosts() 74 | if err != nil { 75 | return "", err 76 | } 77 | return hosts[int(r.rand(uint32(len(hosts))))], nil 78 | } 79 | 80 | type balancer struct { 81 | subscriber Subscriber 82 | } 83 | 84 | func (b *balancer) hosts() ([]string, error) { 85 | hs, err := b.subscriber.Hosts() 86 | if err != nil { 87 | return hs, err 88 | } 89 | if len(hs) <= 0 { 90 | return hs, ErrNoHosts 91 | } 92 | return hs, nil 93 | } 94 | 95 | type nopBalancer string 96 | 97 | func (b nopBalancer) Host() (string, error) { return string(b), nil } 98 | -------------------------------------------------------------------------------- /sd/register.go: -------------------------------------------------------------------------------- 1 | package sd 2 | 3 | import ( 4 | "melody/config" 5 | "melody/register" 6 | ) 7 | 8 | // RegisterSubscriberFactory registers the received factory 9 | func RegisterSubscriberFactory(name string, sf SubscriberFactory) error { 10 | return subscriberFactories.Register(name, sf) 11 | } 12 | 13 | // GetSubscriber 在此处选择调用哪种服务发现 14 | func GetSubscriber(cfg *config.Backend) Subscriber { 15 | return subscriberFactories.Get(cfg.SD)(cfg) 16 | } 17 | 18 | // GetRegister returns the package register 19 | func GetRegister() *Register { 20 | return subscriberFactories 21 | } 22 | 23 | // Register is a SD register 24 | type Register struct { 25 | data register.Untyped 26 | } 27 | 28 | func initRegister() *Register { 29 | return &Register{register.New()} 30 | } 31 | 32 | // Register implements the RegisterSetter interface 33 | func (r *Register) Register(name string, sf SubscriberFactory) error { 34 | r.data.Register(name, sf) 35 | return nil 36 | } 37 | 38 | // Get implements the RegisterGetter interface 39 | func (r *Register) Get(name string) SubscriberFactory { 40 | tmp, ok := r.data.Get(name) 41 | if !ok { 42 | return FixedSubscriberFactory 43 | } 44 | sf, ok := tmp.(SubscriberFactory) 45 | if !ok { 46 | return FixedSubscriberFactory 47 | } 48 | return sf 49 | } 50 | 51 | var subscriberFactories = initRegister() 52 | -------------------------------------------------------------------------------- /sd/subscriber.go: -------------------------------------------------------------------------------- 1 | // Package sd defines some interfaces and implementations for service discovery 2 | package sd 3 | 4 | import "melody/config" 5 | 6 | // Subscriber keeps the set of backend hosts up to date 7 | type Subscriber interface { 8 | Hosts() ([]string, error) 9 | } 10 | 11 | // SubscriberFunc type is an adapter to allow the use of ordinary functions as subscribers. 12 | // If f is a function with the appropriate signature, SubscriberFunc(f) is a Subscriber that calls f. 13 | type SubscriberFunc func() ([]string, error) 14 | 15 | // Hosts implements the Subscriber interface by executing the wrapped function 16 | func (f SubscriberFunc) Hosts() ([]string, error) { return f() } 17 | 18 | // FixedSubscriber has a constant set of backend hosts and they never get updated 19 | type FixedSubscriber []string 20 | 21 | // Hosts implements the subscriber interface 22 | func (s FixedSubscriber) Hosts() ([]string, error) { return s, nil } 23 | 24 | // SubscriberFactory builds subscribers with the received config 25 | type SubscriberFactory func(*config.Backend) Subscriber 26 | 27 | // FixedSubscriberFactory builds a FixedSubscriber with the received config 28 | func FixedSubscriberFactory(cfg *config.Backend) Subscriber { 29 | return FixedSubscriber(cfg.Host) 30 | } 31 | -------------------------------------------------------------------------------- /transport/http/client/executor.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | var defaultHTTPClient = &http.Client{} 9 | 10 | // HTTPRequestExecutor 定义了代理的client,由该方法去实现代理 11 | type HTTPRequestExecutor func(context.Context, *http.Request) (*http.Response, error) 12 | 13 | // HTTPClientFactory 根据context定制client 14 | type HTTPClientFactory func(context.Context) *http.Client 15 | 16 | func NewHTTPClient(context.Context) *http.Client { 17 | return defaultHTTPClient 18 | } 19 | 20 | // DefaultHTTPRequestExecutor 默认的request执行器 21 | func DefaultHTTPRequestExecutor(clientFactory HTTPClientFactory) HTTPRequestExecutor { 22 | return func(ctx context.Context, request *http.Request) (response *http.Response, e error) { 23 | return clientFactory(ctx).Do(request.WithContext(ctx)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /transport/http/client/status.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io/ioutil" 8 | "melody/config" 9 | "net/http" 10 | ) 11 | 12 | const Namespace = "melody_http" 13 | 14 | var ErrInvalidStatusCode = errors.New("Invalid status code") 15 | 16 | // HTTPStatusHandler 将接受到的response中status code格式化 17 | type HTTPStatusHandler func(context.Context, *http.Response) (*http.Response, error) 18 | 19 | // HTTPResponseError 在某个Backend发生错误时,将封装进该对象的实例 20 | type HTTPResponseError struct { 21 | Code int `json:"http_status_code"` 22 | Msg string `json:"http_body,omitempty"` 23 | name string 24 | } 25 | 26 | // NoOpHTTPStatusHandler 空实现 27 | func NoOpHTTPStatusHandler(_ context.Context, resp *http.Response) (*http.Response, error) { 28 | return resp, nil 29 | } 30 | 31 | func GetHTTPStatusHandler(remote *config.Backend) HTTPStatusHandler { 32 | if e, ok := remote.ExtraConfig[Namespace]; ok { 33 | if m, ok := e.(map[string]interface{}); ok { 34 | if v, ok := m["return_error_details"]; ok { 35 | if b, ok := v.(string); ok && b != "" { 36 | return DetailedHTTPStatusHandler(DefaultHTTPStatusHandler, b) 37 | } 38 | } 39 | } 40 | } 41 | return DefaultHTTPStatusHandler 42 | } 43 | 44 | // DetailedHTTPStatusHandler 45 | func DetailedHTTPStatusHandler(next HTTPStatusHandler, name string) HTTPStatusHandler { 46 | return func(ctx context.Context, resp *http.Response) (*http.Response, error) { 47 | if r, err := next(ctx, resp); err == nil { 48 | return r, nil 49 | } 50 | 51 | body, err := ioutil.ReadAll(resp.Body) 52 | if err != nil { 53 | body = []byte{} 54 | } 55 | resp.Body.Close() 56 | resp.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 57 | 58 | return resp, HTTPResponseError{ 59 | Code: resp.StatusCode, 60 | Msg: string(body), 61 | name: name, 62 | } 63 | } 64 | } 65 | 66 | func DefaultHTTPStatusHandler(ctx context.Context, resp *http.Response) (*http.Response, error) { 67 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { 68 | return nil, ErrInvalidStatusCode 69 | } 70 | 71 | return resp, nil 72 | } 73 | 74 | // Error returns the error message 75 | func (r HTTPResponseError) Error() string { 76 | return r.Msg 77 | } 78 | 79 | // Name returns the name of the error 80 | func (r HTTPResponseError) Name() string { 81 | return r.name 82 | } 83 | 84 | // StatusCode returns the status code returned by the backend 85 | func (r HTTPResponseError) StatusCode() int { 86 | return r.Code 87 | } 88 | -------------------------------------------------------------------------------- /transport/http/server/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "melody/register" 6 | "net/http" 7 | ) 8 | 9 | var serverRegister = register.NewSpaces() 10 | 11 | // RegisterHandler ... 12 | func RegisterHandler( 13 | name string, 14 | handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error), 15 | ) { 16 | serverRegister.Register(Namespace, name, handler) 17 | } 18 | -------------------------------------------------------------------------------- /transport/http/server/plugin/server.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "melody/config" 6 | "melody/logging" 7 | "net/http" 8 | ) 9 | 10 | // Namespace 是否运行使用server handler 11 | const Namespace = "http_server_handler" 12 | 13 | // RunServer 定义了运行http.Server的函数结构 14 | type RunServer func(context.Context, config.ServiceConfig, http.Handler) error 15 | 16 | // New 返回下一个RunServer 17 | func New(logger logging.Logger, next RunServer) RunServer { 18 | return func(ctx context.Context, cfg config.ServiceConfig, handler http.Handler) error { 19 | // 根据配置文件检察是否开启handler 20 | v, ok := cfg.ExtraConfig[Namespace] 21 | if !ok { 22 | logger.Debug("http server handler: no extra config") 23 | return next(ctx, cfg, handler) 24 | } 25 | extra, ok := v.(map[string]interface{}) 26 | if !ok { 27 | logger.Debug("http server handler: wrong extra config type") 28 | return next(ctx, cfg, handler) 29 | } 30 | 31 | // 加载插件 32 | r, ok := serverRegister.Get(Namespace) 33 | if !ok { 34 | logger.Debug("http server handler: no plugins registered for the module") 35 | return next(ctx, cfg, handler) 36 | } 37 | 38 | name, nameOk := extra["name"].(string) 39 | fifoRaw, fifoOk := extra["name"].([]interface{}) 40 | if !nameOk && !fifoOk { 41 | logger.Debug("http server handler: no plugins required in the extra config") 42 | return next(ctx, cfg, handler) 43 | } 44 | 45 | fifo := []string{} 46 | 47 | if !fifoOk { 48 | fifo = []string{name} 49 | } else { 50 | for _, x := range fifoRaw { 51 | if v, ok := x.(string); ok { 52 | fifo = append(fifo, v) 53 | } 54 | } 55 | } 56 | 57 | for _, name := range fifo { 58 | rawHf, ok := r.Get(name) 59 | if !ok { 60 | logger.Debug("http server handler: no plugin resgistered as", name) 61 | return next(ctx, cfg, handler) 62 | } 63 | 64 | hf, ok := rawHf.(func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error)) 65 | if !ok { 66 | logger.Warning("http server handler: wrong plugin handler type:", name) 67 | return next(ctx, cfg, handler) 68 | } 69 | 70 | handlerWrapper, err := hf(context.Background(), extra, handler) 71 | if err != nil { 72 | logger.Warning("http server handler: error getting the plugin handler:", err.Error()) 73 | return next(ctx, cfg, handler) 74 | } 75 | 76 | logger.Debug("http server handler: injecting plugin", name) 77 | handler = handlerWrapper 78 | } 79 | 80 | return next(ctx, cfg, handler) 81 | } 82 | } 83 | --------------------------------------------------------------------------------