Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add service package #480

Draft
wants to merge 6 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jobs:
build:
strategy:
matrix:
go-version: [1.14.x, 1.15.x]
go-version: [1.15.x]
os: [ubuntu-latest, macos-latest, windows-latest]

runs-on: ${{ matrix.os }}
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Everything else is the responsibility of the command's author.

If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/?ref=github-webhook-readme). If you have any questions, you can contact us at [email protected]

#

<a href="https://www.hookdeck.io/?ref=adnanh-webhook"><img src="http://hajdarevic.net/hookdeck-logo.svg" height="17" alt="hookdeck" align="left" /></a> If you need a way of inspecting, monitoring and replaying webhooks without the back and forth troubleshooting, [give Hookdeck a try!](https://www.hookdeck.io/?ref=adnanh-webhook)

# Getting started
## Installation
Expand Down Expand Up @@ -129,6 +132,7 @@ Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples
- [Adventures in webhooks](https://medium.com/@draketech/adventures-in-webhooks-2d6584501c62) by [Drake](https://medium.com/@draketech)
- [GitHub pro tips](http://notes.spencerlyon.com/2016/01/04/github-pro-tips/) by [Spencer Lyon](http://notes.spencerlyon.com/)
- [XiaoMi Vacuum + Amazon Button = Dash Cleaning](https://www.instructables.com/id/XiaoMi-Vacuum-Amazon-Button-Dash-Cleaning/) by [c0mmensal](https://www.instructables.com/member/c0mmensal/)
- [Set up Automated Deployments From Github With Webhook](https://maximorlov.com/automated-deployments-from-github-with-webhook/) by [Maxim Orlov](https://twitter.com/_maximization)
- VIDEO: [Gitlab CI/CD configuration using Docker and adnanh/webhook to deploy on VPS - Tutorial #1](https://www.youtube.com/watch?v=Qhn-lXjyrZA&feature=youtu.be) by [Yes! Let's Learn Software Engineering](https://www.youtube.com/channel/UCH4XJf2BZ_52fbf8fOBMF3w)
- ...
- Want to add your own? Open an Issue or create a PR :-)
Expand Down
182 changes: 182 additions & 0 deletions internal/service/security/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Package security provides HTTP security management help to the webhook
// service.
package security

import (
"crypto/tls"
"fmt"
"io"
"log"
"strings"
"sync"
)

// KeyPairReloader contains the active TLS certificate. It can be used with
// the tls.Config.GetCertificate property to support live updating of the
// certificate.
type KeyPairReloader struct {
certMu sync.RWMutex
cert *tls.Certificate
certPath string
keyPath string
}

// NewKeyPairReloader creates a new KeyPairReloader given the certificate and
// key path.
func NewKeyPairReloader(certPath, keyPath string) (*KeyPairReloader, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}

res := &KeyPairReloader{
cert: &cert,
certPath: certPath,
keyPath: keyPath,
}

return res, nil
}

// Reload attempts to reload the TLS key pair.
func (kpr *KeyPairReloader) Reload() error {
cert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
if err != nil {
return err
}

kpr.certMu.Lock()
defer kpr.certMu.Unlock()

kpr.cert = &cert
return nil
}

// GetCertificateFunc provides a function for tls.Config.GetCertificate.
func (kpr *KeyPairReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
kpr.certMu.RLock()
defer kpr.certMu.RUnlock()
return kpr.cert, nil
}
}

// WriteTLSSupportedCipherStrings writes a list of ciphers to w. The list is
// all supported TLS ciphers based upon min.
func WriteTLSSupportedCipherStrings(w io.Writer, min string) error {
m, err := GetTLSVersion(min)
if err != nil {
return err
}

for _, c := range tls.CipherSuites() {
var found bool

for _, v := range c.SupportedVersions {
if v >= m {
found = true
}
}

if !found {
continue
}

_, err := w.Write([]byte(c.Name + "\n"))
if err != nil {
return err
}
}

return nil
}

// GetTLSVersion converts a TLS version string, v, (e.g. "v1.3") into a TLS
// version ID.
func GetTLSVersion(v string) (uint16, error) {
switch v {
case "1.3", "v1.3", "tls1.3":
return tls.VersionTLS13, nil
case "1.2", "v1.2", "tls1.2", "":
return tls.VersionTLS12, nil
case "1.1", "v1.1", "tls1.1":
return tls.VersionTLS11, nil
case "1.0", "v1.0", "tls1.0":
return tls.VersionTLS10, nil
default:
return 0, fmt.Errorf("error: unknown TLS version: %s", v)
}
}

// GetTLSCipherSuites converts a comma separated list of cipher suites into a
// slice of TLS cipher suite IDs.
func GetTLSCipherSuites(v string) []uint16 {
supported := tls.CipherSuites()

if v == "" {
suites := make([]uint16, len(supported))

for _, cs := range supported {
suites = append(suites, cs.ID)
}

return suites
}

var found bool
txts := strings.Split(v, ",")
suites := make([]uint16, len(txts))

for _, want := range txts {
found = false

for _, cs := range supported {
if want == cs.Name {
suites = append(suites, cs.ID)
found = true
}
}

if !found {
log.Fatalln("error: unknown TLS cipher suite:", want)
}
}

return suites
}

// GetTLSCurves converts a comma separated list of curves into a
// slice of TLS curve IDs.
func GetTLSCurves(v string) []tls.CurveID {
supported := []tls.CurveID{
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
tls.X25519,
}

if v == "" {
return supported
}

var found bool
txts := strings.Split(v, ",")
res := make([]tls.CurveID, len(txts))

for _, want := range txts {
found = false

for _, c := range supported {
if want == c.String() {
res = append(res, c)
found = true
}
}

if !found {
log.Fatalln("error: unknown TLS curve:", want)
}
}

return res
}
163 changes: 163 additions & 0 deletions internal/service/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Package service manages the webhook HTTP service.
package service

import (
"crypto/tls"
"fmt"
"net"
"net/http"

"github.com/adnanh/webhook/internal/pidfile"
"github.com/adnanh/webhook/internal/service/security"

"github.com/gorilla/mux"
)

// Service is the webhook HTTP service.
type Service struct {
// Address is the listener address for the service (e.g. "127.0.0.1:9000")
Address string

// TLS settings
enableTLS bool
tlsCiphers []uint16
tlsMinVersion uint16
kpr *security.KeyPairReloader

// Future TLS settings to consider:
// - tlsMaxVersion
// - configurable TLS curves
// - modern and intermediate helpers that follows Mozilla guidelines
// - ca root and intermediate certs

listener net.Listener
server *http.Server

pidFile *pidfile.PIDFile

// Hooks map[string]hook.Hooks
}

// New creates a new webhook HTTP service for the given address and port.
func New(ip string, port int) *Service {
return &Service{
Address: fmt.Sprintf("%s:%d", ip, port),
server: &http.Server{},
tlsMinVersion: tls.VersionTLS12,
}
}

// Listen announces the TCP service on the local network.
//
// To enable TLS, ensure that SetTLSEnabled is called prior to Listen.
//
// After calling Listen, Serve must be called to begin serving HTTP requests.
// The steps are separated so that we can drop privileges, if necessary, after
// opening the listening port.
func (s *Service) Listen() error {
ln, err := net.Listen("tcp", s.Address)
if err != nil {
return err
}

if !s.enableTLS {
s.listener = ln
return nil
}

if s.kpr == nil {
panic("Listen called with TLS enabled but KPR is nil")
}

c := &tls.Config{
GetCertificate: s.kpr.GetCertificateFunc(),
CipherSuites: s.tlsCiphers,
CurvePreferences: security.GetTLSCurves(""),
MinVersion: s.tlsMinVersion,
PreferServerCipherSuites: true,
}

s.listener = tls.NewListener(ln, c)

return nil
}

// Serve begins accepting incoming HTTP connections.
func (s *Service) Serve() error {
s.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) // disable http/2

if s.listener == nil {
err := s.Listen()
if err != nil {
return err
}
}

defer s.listener.Close()
return s.server.Serve(s.listener)
}

// SetHTTPHandler sets the underly HTTP server Handler.
func (s *Service) SetHTTPHandler(r *mux.Router) {
s.server.Handler = r
}

// SetTLSCiphers sets the supported TLS ciphers.
func (s *Service) SetTLSCiphers(suites string) {
s.tlsCiphers = security.GetTLSCipherSuites(suites)
}

// SetTLSEnabled enables TLS for the service. Must be called prior to Listen.
func (s *Service) SetTLSEnabled() {
s.enableTLS = true
}

// TLSEnabled return true if TLS is enabled for the service.
func (s *Service) TLSEnabled() bool {
return s.enableTLS
}

// SetTLSKeyPair sets the TLS key pair for the service.
func (s *Service) SetTLSKeyPair(certPath, keyPath string) error {
if certPath == "" {
return fmt.Errorf("error: certificate required for TLS")
}

if keyPath == "" {
return fmt.Errorf("error: key required for TLS")
}

var err error

s.kpr, err = security.NewKeyPairReloader(certPath, keyPath)
if err != nil {
return err
}

return nil
}

// ReloadTLSKeyPair attempts to reload the configured TLS certificate key pair.
func (s *Service) ReloadTLSKeyPair() error {
return s.kpr.Reload()
}

// SetTLSMinVersion sets the minimum support TLS version, such as "v1.3".
func (s *Service) SetTLSMinVersion(ver string) (err error) {
s.tlsMinVersion, err = security.GetTLSVersion(ver)
return err
}

// CreatePIDFile creates a new PID file at path p.
func (s *Service) CreatePIDFile(p string) (err error) {
s.pidFile, err = pidfile.New(p)
return err
}

// DeletePIDFile deletes a previously created PID file.
func (s *Service) DeletePIDFile() error {
if s.pidFile != nil {
return s.pidFile.Remove()
}
return nil
}