├── .dockerignore ├── .gitignore ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── fly.toml ├── go.mod ├── go.sum ├── main.go └── server.go /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/*.log 3 | **/*.json 4 | **/github-events 5 | **/bin 6 | **/bin/* 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | DS_Store 2 | *.log 3 | *.json 4 | github-events 5 | bin 6 | bin/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean build 2 | 3 | build: 4 | go build 5 | 6 | clean: 7 | rm -f *.json 8 | rm -f *.log 9 | rm -f ./bin/* 10 | 11 | release: 12 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ./bin/github-events_linux_amd64 13 | GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o ./bin/github-events_darwin_amd64 14 | GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o ./bin/github-events_darwin_arm64 15 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: github-events -server -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-events 2 | 3 | A CLI tool to subscribe to Github events from your local repository 4 | 5 | ## Overview 6 | 7 | `github-events` utility will create a temporary webhook for your Github repository 8 | and print out all event payloads as they come in the real time. Once the process is 9 | stopped the temporary webhook is destroyed. This is super useful when testing Github 10 | events as there's no need to setup anything (endpoints, webhook receivers, etc). 11 | 12 | It works by creating a temporary webhook for the repository that points to an 13 | URL like `https://github-events.fly.dev/key`, where `key` is a random 14 | token. Github will be sending all events to that URL moving forward. Then utility 15 | connects to the given URL using Websocket protocol and receives all events in JSON 16 | format. Server part is open, check `server.go` file. 17 | 18 | ## Installation 19 | 20 | If you have Go installed locally, run: 21 | 22 | ``` 23 | go get github.com/sosedoff/github-events 24 | ``` 25 | 26 | Or visit [Releases](https://github.com/sosedoff/github-events/releases) page to grab a binary. 27 | 28 | ## Configuration 29 | 30 | There are two ways how you can configure the `github-events`: 31 | 32 | 1. Environment variable 33 | 34 | [Create a personal token](https://github.com/settings/tokens/new) first, then start 35 | the process with: 36 | 37 | ``` 38 | GITHUB_TOKEN=... github-events 39 | ``` 40 | 41 | 2. Netrc entry 42 | 43 | Add a following record to the `~/.netrc` file: 44 | 45 | ``` 46 | machine api.github.com 47 | login YOUR_GITHUB_LOGIN 48 | password YOUR_GITHUB_PERSONAL_TOKEN 49 | ``` 50 | 51 | ## Usage 52 | 53 | See application usage with `github-events -h`: 54 | 55 | ``` 56 | Usage of ./github-events: 57 | -endpoint string 58 | Set custom server endpoint 59 | -forward string 60 | URL to forward events to 61 | -only string 62 | Filter events by type 63 | -pretty 64 | Pretty print JSON 65 | -repo string 66 | Repository name (namespace/repo) 67 | -save 68 | Save each event into separate file 69 | -server 70 | Start server 71 | ``` 72 | 73 | Some of the use cases: 74 | 75 | ```bash 76 | # Pipe to jq for pretty printing and colorization 77 | github-events | jq 78 | 79 | # Or use internal pretty print option 80 | github-events -pretty 81 | 82 | # Save to file 83 | github-events > events.log 84 | 85 | # Filter by event type 86 | github-events -only=push 87 | 88 | # Save each event to a file. 89 | # They are still printed out to STDOUT. 90 | github-events -save -pretty 91 | ``` 92 | 93 | You can also forward event data to a local HTTP endpoint: 94 | 95 | ```bash 96 | # lets say you have an app running on localhost:5000 97 | # forward requests by running this command 98 | github-events -forward http://locahost:5000/events 99 | ``` 100 | 101 | To watch events from a repository that's not cloned in the same directory: 102 | 103 | ```bash 104 | github-events -repo yourname/reponame 105 | ``` 106 | 107 | While the event proxy server is hosted on Heroku, you can run the server locally: 108 | 109 | ```bash 110 | github-events -server 111 | ``` 112 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | app = "github-events" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [build] 7 | builder = "paketobuildpacks/builder:base" 8 | buildpacks = ["gcr.io/paketo-buildpacks/go"] 9 | 10 | [env] 11 | PORT = "8080" 12 | 13 | [experimental] 14 | allowed_public_ports = [] 15 | auto_rollback = true 16 | 17 | [[services]] 18 | http_checks = [] 19 | internal_port = 8080 20 | processes = ["app"] 21 | protocol = "tcp" 22 | script_checks = [] 23 | 24 | [services.concurrency] 25 | hard_limit = 25 26 | soft_limit = 20 27 | type = "connections" 28 | 29 | [[services.ports]] 30 | force_https = true 31 | handlers = ["http"] 32 | port = 80 33 | 34 | [[services.ports]] 35 | handlers = ["tls", "http"] 36 | port = 443 37 | 38 | [[services.tcp_checks]] 39 | grace_period = "1s" 40 | interval = "15s" 41 | restart_limit = 0 42 | timeout = "2s" 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Heroku config: https://github.com/heroku/heroku-buildpack-go 2 | // +heroku goVersion go1.19 3 | 4 | module github.com/sosedoff/github-events 5 | 6 | go 1.19 7 | 8 | require ( 9 | github.com/gin-gonic/gin v1.8.1 10 | github.com/google/go-github v17.0.0+incompatible 11 | github.com/gorilla/websocket v1.5.0 12 | github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 13 | golang.org/x/oauth2 v0.3.0 14 | ) 15 | 16 | require ( 17 | github.com/gin-contrib/sse v0.1.0 // indirect 18 | github.com/go-playground/locales v0.14.0 // indirect 19 | github.com/go-playground/universal-translator v0.18.0 // indirect 20 | github.com/go-playground/validator/v10 v10.11.1 // indirect 21 | github.com/goccy/go-json v0.10.0 // indirect 22 | github.com/golang/protobuf v1.5.2 // indirect 23 | github.com/google/go-querystring v1.1.0 // indirect 24 | github.com/json-iterator/go v1.1.12 // indirect 25 | github.com/leodido/go-urn v1.2.1 // indirect 26 | github.com/mattn/go-isatty v0.0.16 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 30 | github.com/ugorji/go/codec v1.2.7 // indirect 31 | golang.org/x/crypto v0.4.0 // indirect 32 | golang.org/x/net v0.4.0 // indirect 33 | golang.org/x/sys v0.3.0 // indirect 34 | golang.org/x/text v0.5.0 // indirect 35 | google.golang.org/appengine v1.6.7 // indirect 36 | google.golang.org/protobuf v1.28.1 // indirect 37 | gopkg.in/yaml.v2 v2.4.0 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 7 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 8 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= 9 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 10 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 11 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 12 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 13 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 14 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 15 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 16 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 17 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 18 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 19 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 20 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 21 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 22 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 23 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 24 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= 25 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 26 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 27 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 31 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 32 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 33 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 34 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 35 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 38 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 39 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 40 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 41 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 42 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 43 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 44 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 45 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 46 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 47 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 48 | github.com/jdxcode/netrc v0.0.0-20190329161231-b36f1c51d91d h1:Io4Ts9W/92wkP9VKQTbGpY5VczamXILBrpz490a1/vw= 49 | github.com/jdxcode/netrc v0.0.0-20190329161231-b36f1c51d91d/go.mod h1:PSWm5RA4GUQ+cyCXiBIIUjlDWdJci5cU3GVKwaQRmW8= 50 | github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co= 51 | github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= 52 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 53 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 54 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 55 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 56 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 57 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 58 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 59 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 60 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 61 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 62 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 63 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 64 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 65 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 66 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 67 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 68 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 69 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 70 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 71 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 73 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 74 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 75 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 76 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 77 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 78 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 79 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 80 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 81 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 82 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 83 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 85 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 86 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 87 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 88 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 89 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 90 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 91 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 92 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 93 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 94 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 96 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 97 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 98 | github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= 99 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 100 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 101 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 102 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 103 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 104 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 105 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 106 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 107 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 108 | golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= 109 | golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= 110 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 111 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 112 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 113 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 114 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 115 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 116 | golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= 117 | golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 118 | golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c h1:HjRaKPaiWks0f5tA6ELVF7ZfqSppfPwOEEAvsrKUTO4= 119 | golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 120 | golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= 121 | golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= 122 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 124 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 126 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 133 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 135 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 136 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 137 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 138 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 139 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 140 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 141 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 142 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 144 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 145 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 146 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 147 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 148 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 149 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 150 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 151 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 152 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 153 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 154 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 155 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 156 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 157 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 158 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 159 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 160 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 161 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 162 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 163 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 164 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "encoding/hex" 8 | "encoding/json" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | "os" 16 | "os/exec" 17 | "os/signal" 18 | "path/filepath" 19 | "regexp" 20 | "strings" 21 | "time" 22 | 23 | "github.com/google/go-github/github" 24 | "github.com/gorilla/websocket" 25 | "github.com/jdxcode/netrc" 26 | "golang.org/x/oauth2" 27 | ) 28 | 29 | var ( 30 | version = "0.3.0" 31 | proxyEndpoint = "https://github-events.fly.dev" 32 | 33 | reRepoHTTP = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`) 34 | reRepoSSH = regexp.MustCompile(`github.com[:/](.+)/(.+).git$`) 35 | reEventHook = regexp.MustCompile(proxyEndpoint + `/(.*)`) 36 | ) 37 | 38 | // Message contains github event data 39 | type Message struct { 40 | Event string `json:"event"` 41 | Payload json.RawMessage `json:"payload"` 42 | } 43 | 44 | // randomHex returns a random hex string 45 | func randomHex(n int) (string, error) { 46 | bytes := make([]byte, n) 47 | if _, err := rand.Read(bytes); err != nil { 48 | return "", err 49 | } 50 | return hex.EncodeToString(bytes), nil 51 | } 52 | 53 | // getRepo returns a owner, repo and an error 54 | func getRepo(remote string) (string, string, error) { 55 | outBuf := bytes.NewBuffer(nil) 56 | errBuf := bytes.NewBuffer(nil) 57 | 58 | cmd := exec.Command("git", "remote", "get-url", remote) 59 | cmd.Stdout = outBuf 60 | cmd.Stderr = errBuf 61 | 62 | if err := cmd.Run(); err != nil { 63 | return "", "", err 64 | } 65 | 66 | output := strings.TrimSpace(outBuf.String()) 67 | 68 | matches := reRepoSSH.FindAllStringSubmatch(output, 1) 69 | if len(matches) > 0 { 70 | return matches[0][1], matches[0][2], nil 71 | } 72 | 73 | matches = reRepoHTTP.FindAllStringSubmatch(output, 1) 74 | if len(matches) > 0 { 75 | return matches[0][1], matches[0][2], nil 76 | } 77 | 78 | return "", "", errors.New("Git remote does not belong to Github") 79 | } 80 | 81 | func githubClientFromEnv() (*github.Client, error) { 82 | token := os.Getenv("GITHUB_TOKEN") 83 | 84 | if token == "" { 85 | path := filepath.Join(os.Getenv("HOME"), ".netrc") 86 | 87 | rc, err := netrc.Parse(path) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | machine := rc.Machine("api.github.com") 93 | if machine != nil { 94 | token = machine.Get("password") 95 | } 96 | } 97 | 98 | if token == "" { 99 | return nil, errors.New("Github API token is not set") 100 | } 101 | 102 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 103 | tc := oauth2.NewClient(context.Background(), ts) 104 | 105 | return github.NewClient(tc), nil 106 | } 107 | 108 | func startWebsocketPing(conn *websocket.Conn, done chan bool) { 109 | for { 110 | select { 111 | case <-done: 112 | return 113 | case <-time.Tick(time.Second * 5): 114 | if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { 115 | log.Println("Websocket ping error:", err) 116 | } 117 | } 118 | } 119 | } 120 | 121 | func forwardMessage(url string, message Message) { 122 | body := bytes.NewReader(message.Payload) 123 | req, err := http.NewRequest(http.MethodPost, url, body) 124 | if err != nil { 125 | log.Println("Request setup error:", err) 126 | return 127 | } 128 | 129 | req.Header.Set("Content-Type", "application/json") 130 | req.Header.Set("X-GitHub-Delivery", "1") 131 | req.Header.Set("X-Github-Event", message.Event) 132 | 133 | resp, err := http.DefaultClient.Do(req) 134 | if err != nil { 135 | log.Println("Request error:", err) 136 | return 137 | } 138 | defer resp.Body.Close() 139 | 140 | respBody, err := ioutil.ReadAll(resp.Body) 141 | if err != nil { 142 | log.Println("Body read error:", err) 143 | } 144 | 145 | log.Println("Forwarded response:", resp.StatusCode) 146 | 147 | if resp.StatusCode >= 300 { 148 | log.Printf("Forwarded response body:\n%s\n", respBody) 149 | } 150 | } 151 | 152 | func startServer() { 153 | addr := getListenAddr("PORT", "5000") 154 | server := newServer() 155 | 156 | log.Println("Starting server on", addr) 157 | if err := server.Run(addr); err != nil { 158 | log.Fatal(err) 159 | } 160 | } 161 | 162 | func main() { 163 | var runServer bool 164 | var repoName string 165 | var filterType string 166 | var pretty bool 167 | var saveFiles bool 168 | var endpoint string 169 | var forwardURL string 170 | 171 | flag.BoolVar(&runServer, "server", false, "Start server") 172 | flag.StringVar(&repoName, "repo", "", "Repository name (namespace/repo)") 173 | flag.StringVar(&filterType, "only", "", "Filter events by type") 174 | flag.BoolVar(&pretty, "pretty", false, "Pretty print JSON") 175 | flag.BoolVar(&saveFiles, "save", false, "Save each event into separate file") 176 | flag.StringVar(&endpoint, "endpoint", "", "Set custom server endpoint") 177 | flag.StringVar(&forwardURL, "forward", "", "URL to forward events to") 178 | flag.Parse() 179 | 180 | if runServer { 181 | startServer() 182 | return 183 | } 184 | 185 | if endpoint != "" { 186 | proxyEndpoint = endpoint 187 | reEventHook = regexp.MustCompile(proxyEndpoint + `/(.*)`) 188 | } 189 | 190 | log.Println("Configuring Github API client") 191 | client, err := githubClientFromEnv() 192 | if err != nil { 193 | log.Fatal(err) 194 | } 195 | 196 | var owner, repo string 197 | if repoName == "" { 198 | log.Println("Inspecting git remote") 199 | owner, repo, err = getRepo("origin") 200 | if err != nil { 201 | log.Fatal(err) 202 | } 203 | } else { 204 | chunks := strings.SplitN(repoName, "/", 2) 205 | if len(chunks) < 2 { 206 | log.Fatal("Invalid repo name") 207 | } 208 | owner = chunks[0] 209 | repo = chunks[1] 210 | } 211 | 212 | log.Println("Fetching existing webhooks") 213 | hooks, _, err := client.Repositories.ListHooks(context.Background(), owner, repo, &github.ListOptions{}) 214 | if err != nil { 215 | log.Fatal(err) 216 | } 217 | for _, hook := range hooks { 218 | if url, ok := hook.Config["url"].(string); ok { 219 | if reEventHook.MatchString(url) { 220 | log.Println("Removing the existing webhook:", *hook.ID) 221 | _, err := client.Repositories.DeleteHook(context.Background(), owner, repo, *hook.ID) 222 | if err != nil { 223 | log.Fatal(err) 224 | } 225 | log.Println("Existing webhook has been removed") 226 | } 227 | } 228 | } 229 | 230 | log.Println("Generating a new key") 231 | key, err := randomHex(20) 232 | if err != nil { 233 | log.Fatal(err) 234 | } 235 | 236 | hookURL := fmt.Sprintf("%s/%s", proxyEndpoint, key) 237 | 238 | hook := github.Hook{} 239 | hook.Events = []string{"*"} 240 | hook.Config = map[string]interface{}{ 241 | "url": hookURL, 242 | "content_type": "json", 243 | } 244 | 245 | log.Println("Creating a new webhook") 246 | newhook, _, err := client.Repositories.CreateHook(context.Background(), owner, repo, &hook) 247 | if err != nil { 248 | log.Fatal(err) 249 | } 250 | 251 | defer func() { 252 | log.Println("Removing the webhook") 253 | _, err := client.Repositories.DeleteHook(context.Background(), owner, repo, *newhook.ID) 254 | if err != nil { 255 | log.Println("Failed to remove hook:", err) 256 | } 257 | }() 258 | 259 | wsURL := strings.Replace(hookURL, "https:", "wss:", 1) 260 | conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) 261 | if err != nil { 262 | log.Fatal(err) 263 | } 264 | defer conn.Close() 265 | 266 | pingClose := make(chan bool) 267 | defer func() { 268 | pingClose <- true 269 | }() 270 | go startWebsocketPing(conn, pingClose) 271 | 272 | log.Println("Listening to events") 273 | go func() { 274 | var message Message 275 | 276 | for { 277 | mtype, data, err := conn.ReadMessage() 278 | if err != nil { 279 | log.Println("Websocket read error:", err) 280 | break 281 | } 282 | if mtype != websocket.TextMessage { 283 | continue 284 | } 285 | 286 | if err := json.Unmarshal(data, &message); err != nil { 287 | log.Println("JSON error:", err) 288 | continue 289 | } 290 | 291 | log.Println("Received event:", message.Event) 292 | 293 | if filterType != "" && message.Event != filterType { 294 | log.Println("Skipped:", message.Event) 295 | continue 296 | } 297 | 298 | if pretty { 299 | newdata, err := json.MarshalIndent(message, "", " ") 300 | if err == nil { 301 | data = newdata 302 | } 303 | } 304 | 305 | fmt.Printf("%s", data) 306 | 307 | if saveFiles { 308 | path := fmt.Sprintf("%v.%s.json", time.Now().UnixNano(), message.Event) 309 | if err := ioutil.WriteFile(path, data, 0666); err != nil { 310 | log.Println("File save error:", err) 311 | } 312 | } 313 | 314 | if forwardURL != "" { 315 | go forwardMessage(forwardURL, message) 316 | } 317 | } 318 | }() 319 | 320 | c := make(chan os.Signal, 1) 321 | signal.Notify(c, os.Interrupt, os.Kill) 322 | <-c 323 | } 324 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/gorilla/websocket" 13 | ) 14 | 15 | type server struct { 16 | clients map[*websocket.Conn]string 17 | clientsLock sync.Mutex 18 | upgrader websocket.Upgrader 19 | } 20 | 21 | func (s *server) broadcast(key string, message interface{}) { 22 | s.clientsLock.Lock() 23 | for clientConn, clientKey := range s.clients { 24 | if clientKey == key { 25 | clientConn.WriteJSON(message) 26 | } 27 | } 28 | s.clientsLock.Unlock() 29 | } 30 | 31 | func (s *server) addClient(conn *websocket.Conn, key string) { 32 | s.clientsLock.Lock() 33 | s.clients[conn] = key 34 | s.clientsLock.Unlock() 35 | } 36 | 37 | func (s *server) removeClient(conn *websocket.Conn, key string) { 38 | s.clientsLock.Lock() 39 | delete(s.clients, conn) 40 | s.clientsLock.Unlock() 41 | } 42 | 43 | func (s *server) handleEvent(c *gin.Context) { 44 | key := c.Param("key") 45 | 46 | // Must be coming from Github 47 | if c.GetHeader("X-GitHub-Delivery") == "" { 48 | c.JSON(400, gin.H{"error": "X-GitHub-Delivery header is not set"}) 49 | return 50 | } 51 | 52 | // Require an event name 53 | event := c.GetHeader("X-GitHub-Event") 54 | if event == "" { 55 | c.JSON(400, gin.H{"error": "X-GitHub-Event header is not set"}) 56 | return 57 | } 58 | 59 | var msg json.RawMessage 60 | 61 | decoder := json.NewDecoder(c.Request.Body) 62 | if err := decoder.Decode(&msg); err != nil { 63 | c.JSON(400, gin.H{"error": err.Error()}) 64 | return 65 | } 66 | 67 | if len(s.clients) > 0 { 68 | go s.broadcast(key, gin.H{"event": event, "payload": msg}) 69 | } 70 | 71 | c.JSON(200, gin.H{"accepted": true}) 72 | } 73 | 74 | func (s *server) handleListen(c *gin.Context) { 75 | conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil) 76 | if err != nil { 77 | log.Println("connection upgrade error:", err) 78 | return 79 | } 80 | defer conn.Close() 81 | 82 | key := c.Param("key") 83 | 84 | s.addClient(conn, key) 85 | defer s.removeClient(conn, key) 86 | 87 | for { 88 | if _, _, err := conn.ReadMessage(); err != nil { 89 | log.Println("read error:", err) 90 | break 91 | } 92 | } 93 | } 94 | 95 | func getListenAddr(key string, port string) string { 96 | listenPort := os.Getenv(key) 97 | if listenPort == "" { 98 | listenPort = port 99 | } 100 | return "0.0.0.0:" + listenPort 101 | } 102 | 103 | func newServer() *gin.Engine { 104 | gin.SetMode(gin.ReleaseMode) 105 | 106 | srv := server{ 107 | clients: map[*websocket.Conn]string{}, 108 | clientsLock: sync.Mutex{}, 109 | upgrader: websocket.Upgrader{ 110 | ReadBufferSize: 1024, 111 | WriteBufferSize: 1024, 112 | HandshakeTimeout: time.Second * 5, 113 | CheckOrigin: func(r *http.Request) bool { 114 | return true 115 | }, 116 | }, 117 | } 118 | 119 | router := gin.Default() 120 | router.POST("/:key", srv.handleEvent) 121 | router.GET("/:key", srv.handleListen) 122 | 123 | return router 124 | } 125 | --------------------------------------------------------------------------------