commit 09aa9db87601fbdcf6cdf27068de65e448ab05b3
Author: Yongbin Kim <iam@yongbin.kim>
Date: Fri, 23 Sep 2022 14:16:53 +0900
first commit
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
14 files changed, 698 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,196 @@
+# TLS related files
+*.pem
+*.cert
+*.key
+
+# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,goland
+# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,goland
+
+### GoLand ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### GoLand Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+# Azure Toolkit for IntelliJ plugin
+# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
+.idea/**/azureSettings.xml
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,goland
diff --git a/.idea/.gitignore b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/devroxy.iml b/.idea/devroxy.iml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+ <component name="Go" enabled="true">
+ <buildTags>
+ <option name="customFlags">
+ <array>
+ <option value="wireinject" />
+ </array>
+ </option>
+ </buildTags>
+ </component>
+ <component name="NewModuleRootManager">
+ <content url="file://$MODULE_DIR$" />
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
+\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/.idea/devroxy.iml" filepath="$PROJECT_DIR$/.idea/devroxy.iml" />
+ </modules>
+ </component>
+</project>
+\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project>
+\ No newline at end of file
diff --git a/Makefile b/Makefile
@@ -0,0 +1,13 @@
+BUILD_OUT := builds/devroxy
+
+.PHONY := help
+.DEFAULT_GOAL := help
+help:
+ @echo TBD
+
+build:
+ mkdir -p $(dir ${BUILD_OUT})
+ go build -o ${BUILD_OUT} .
+
+run:
+ go run . -binds binds.yaml -cert cert.pem -key key.pem
diff --git a/README b/README
@@ -0,0 +1,40 @@
+# Devroxy
+
+Simple http(s) proxy for localhost development.
+
+# Getting started
+
+I recommend that you set up a DNS server on localhost.
+
+```
+*.internal. IN A 127.0.0.1
+```
+
+`/etc/hosts` can be used if you prefer this way.
+
+```
+api.internal 127.0.0.1
+app.internal 127.0.0.1
+...
+```
+
+Devroxy does not matter what you use.
+
+After you setup the dns server, starts devroxy server with command below:
+
+```
+
+sudo devroxy serve
+
+# Port 7199 is in use?
+devroxy serve -p 8080
+```
+
+and bind the domain to destination port.
+
+```
+# You can use any http client, even the browser.
+http://localhost:7199/bind?api.internal=8080
+```
+
+Done! Now you can access to `http://api.internal`, devroxy will proxy to port 8080.
diff --git a/go.mod b/go.mod
@@ -0,0 +1,12 @@
+module devroxy
+
+go 1.18
+
+require (
+ github.com/google/wire v0.5.0 // indirect
+ github.com/mattn/go-colorable v0.1.12 // indirect
+ github.com/mattn/go-isatty v0.0.14 // indirect
+ github.com/rs/zerolog v1.28.0 // indirect
+ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,26 @@
+github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
+github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
+github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
+github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/devroxy/api.go b/internal/devroxy/api.go
@@ -0,0 +1,55 @@
+package devroxy
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (d *Devroxy) handleListBinds(w http.ResponseWriter, _ *http.Request) {
+ sendJSON(w, http.StatusOK, d.binds)
+}
+
+func (d *Devroxy) handleRegisterBind(w http.ResponseWriter, r *http.Request) {
+ query := r.URL.Query()
+
+ domain := query.Get("domain")
+ port, err := strconv.ParseInt(query.Get("port"), 10, 32)
+ if err != nil {
+ sendMessage(w, http.StatusBadRequest, "invalid port number")
+ return
+ }
+
+ d.registerBind(domain, int(port))
+ sendOK(w)
+}
+
+func (d *Devroxy) handleRemoveBind(w http.ResponseWriter, r *http.Request) {
+ query := r.URL.Query()
+ domain := query.Get("domain")
+ d.removeBind(domain)
+ sendOK(w)
+}
+
+func (d *Devroxy) handleSaveBinds(w http.ResponseWriter, _ *http.Request) {
+ err := d.saveBinds()
+ if err != nil {
+ log.Err(err).Msg("binds/save: save failed")
+ sendError(w, http.StatusBadRequest, err)
+ return
+ }
+ sendOK(w)
+}
+
+func (d *Devroxy) handleNotFound(w http.ResponseWriter, _ *http.Request) {
+ sendMessage(w, http.StatusNotFound, "not found")
+}
+
+func (d *Devroxy) handleProxyError(w http.ResponseWriter, r *http.Request, err error) {
+ log.Err(err).
+ Str("host", r.Host).
+ Str("path", r.URL.Path).
+ Msg("proxy error")
+ sendError(w, http.StatusBadGateway, err)
+}
diff --git a/internal/devroxy/binds.go b/internal/devroxy/binds.go
@@ -0,0 +1,54 @@
+package devroxy
+
+import (
+ "fmt"
+ "os"
+
+ "gopkg.in/yaml.v3"
+)
+
+func loadBindImpl(out map[string]int, bindsFile string) error {
+ fp, err := os.Open(bindsFile)
+ if err != nil {
+ return err
+ }
+ defer fp.Close()
+
+ err = yaml.NewDecoder(fp).Decode(out)
+ if err != nil {
+ return fmt.Errorf("failed to decode bind file '%q': %w", bindsFile, err)
+ }
+
+ return nil
+}
+
+func (d *Devroxy) registerBind(domain string, port int) {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ d.binds[domain] = port
+}
+
+func (d *Devroxy) removeBind(domain string) {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ delete(d.binds, domain)
+}
+
+func (d *Devroxy) saveBinds() error {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ // is in-memory only?
+ if len(d.bindsDest) == 0 {
+ return nil
+ }
+
+ data, err := yaml.Marshal(d.binds)
+ if err != nil {
+ return err
+ }
+
+ return os.WriteFile(d.bindsDest, data, 0644)
+}
diff --git a/internal/devroxy/devroxy.go b/internal/devroxy/devroxy.go
@@ -0,0 +1,95 @@
+package devroxy
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httputil"
+ "sync"
+
+ "github.com/rs/zerolog/log"
+)
+
+type Devroxy struct {
+ mutex sync.RWMutex
+
+ proxyHandler *httputil.ReverseProxy
+ mux *http.ServeMux
+
+ binds map[string]int
+ bindsDest string
+
+ internalIP string
+ internalAddr string
+}
+
+func New(addr string) *Devroxy {
+ ip, port, err := parseAddr(addr)
+ if err != nil {
+ log.Fatal().Str("addr", addr).Err(err).Msg("devroxy: invalid address")
+ }
+
+ return (&Devroxy{
+ binds: make(map[string]int),
+
+ internalIP: ip,
+ internalAddr: fmt.Sprintf("%s:%d", ip, port),
+ }).setup()
+}
+
+func (d *Devroxy) setup() *Devroxy {
+ d.proxyHandler = &httputil.ReverseProxy{
+ Director: d.director,
+ ErrorHandler: d.handleProxyError,
+ }
+
+ // select * from table where cond=1 order by asc desc;
+
+ d.mux = http.NewServeMux()
+ d.mux.HandleFunc("/devroxy/binds", d.handleListBinds)
+ d.mux.HandleFunc("/devroxy/binds/register", d.handleRegisterBind)
+ d.mux.HandleFunc("/devroxy/binds/remove", d.handleRemoveBind)
+ d.mux.HandleFunc("/devroxy/binds/save", d.handleSaveBinds)
+ // d.mux.HandleFunc("/devroxy/not-found", d.handleNotFound)
+ d.mux.HandleFunc("/devroxy/", d.handleNotFound)
+ d.mux.Handle("/", d.proxyHandler)
+
+ return d
+}
+
+func (d *Devroxy) Handler() http.Handler {
+ d.mutex.RLock()
+ defer d.mutex.RUnlock()
+ return d.mux
+}
+
+func (d *Devroxy) LoadBinds(bindsFile string) error {
+ d.bindsDest = bindsFile
+ return loadBindImpl(d.binds, bindsFile)
+}
+
+func (d *Devroxy) director(r *http.Request) {
+ d.mutex.RLock()
+ defer d.mutex.RUnlock()
+
+ var dest string
+ if port, ok := d.binds[r.Host]; ok {
+ dest = fmt.Sprintf("%s:%d", d.internalIP, port)
+ } else {
+ dest = d.internalAddr
+ r.URL.Path = "/devroxy/not-found"
+ r.URL.RawPath = "/devroxy/not-found"
+ r.URL.RawQuery = ""
+ }
+
+ log.Info().
+ Str("host", r.Host).
+ Str("path", r.URL.Path).
+ Msg("proxy requested")
+
+ r.URL.Scheme = "http"
+ r.URL.Host = dest
+
+ if _, ok := r.Header["User-Agent"]; !ok {
+ r.Header.Set("User-Agent", "")
+ }
+}
diff --git a/internal/devroxy/utils.go b/internal/devroxy/utils.go
@@ -0,0 +1,66 @@
+package devroxy
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+func sendOK(w http.ResponseWriter) {
+ sendMessage(w, http.StatusOK, "ok")
+}
+
+func sendMessage(w http.ResponseWriter, status int, message string) {
+ sendJSON(w, status, struct {
+ Message string `json:"message"`
+ }{
+ Message: message,
+ })
+}
+
+func sendError(w http.ResponseWriter, status int, err error) {
+ sendJSON(w, status, struct {
+ Error string `json:"error"`
+ }{
+ Error: err.Error(),
+ })
+}
+
+func sendJSON(w http.ResponseWriter, status int, v any) {
+ w.Header().Set("Content-Type", "application/json;charset=utf-8")
+
+ data, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ log.Err(err).Msg("sendJSON: response marshaling failed")
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(`{"message":"internal error"}`))
+ return
+ }
+
+ w.WriteHeader(status)
+ w.Write(data)
+}
+
+func parseAddr(addr string) (string, int, error) {
+ i := strings.LastIndexByte(addr, ':')
+ if i < 0 || i == len(addr)-1 {
+ return "", 0, errors.New("invalid address")
+ }
+
+ port, err := strconv.ParseInt(addr[i+1:], 10, 32)
+ if err != nil || port < 10 || port > 65535 {
+ return "", 0, fmt.Errorf("invalid port number: %s", addr[i+1:])
+ }
+
+ ip := addr[:i]
+ if len(ip) == 0 {
+ ip = "127.0.0.1"
+ }
+
+ return ip, int(port), nil
+}
diff --git a/main.go b/main.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "devroxy/internal/devroxy"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+)
+
+var (
+ flagAddr = flag.String("addr", "", "Address that server listening for. if omitted, server uses default port (:80 or :443)")
+ flagBinds = flag.String("binds", "", "Bind file location. if omitted, devroxy runs in-memory mode.")
+ flagCert = flag.String("cert", "", "SSL certification. if omitted, server will listen for http request.")
+ flagKey = flag.String("key", "", "SSL key. if omitted, server will listen for http request.")
+)
+
+const (
+ ReadTimeout = time.Second
+ WriteTimeout = time.Second
+)
+
+func main() {
+ flag.Parse()
+ zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
+
+ useTLS := len(*flagCert) > 0 && len(*flagKey) > 0
+ if !useTLS && (len(*flagCert) > 0 && len(*flagKey) > 0) {
+
+ }
+ if len(*flagAddr) == 0 {
+ if useTLS {
+ *flagAddr = ":80"
+ } else {
+ *flagAddr = ":443"
+ }
+ }
+
+ d := devroxy.New(*flagAddr)
+
+ if len(*flagBinds) > 0 {
+ err := d.LoadBinds(*flagBinds)
+ if err != nil && !os.IsNotExist(err) {
+ log.Fatal().Err(err).Msg("failed to load binds file")
+ }
+ }
+
+ server := &http.Server{
+ Addr: *flagAddr,
+ Handler: d.Handler(),
+ ReadTimeout: ReadTimeout,
+ WriteTimeout: WriteTimeout,
+ }
+
+ errChan := make(chan error, 1)
+ go func() {
+ log.Info().
+ Str("addr", *flagAddr).
+ Bool("tls", useTLS).
+ Msg("server started")
+
+ var err error
+ if useTLS {
+ err = server.ListenAndServe()
+ } else {
+ err = server.ListenAndServeTLS(*flagCert, *flagKey)
+ }
+
+ if err != http.ErrServerClosed {
+ errChan <- err
+ }
+ }()
+
+ sigChan := make(chan os.Signal)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+ select {
+ case err := <-errChan:
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to start the server")
+ }
+
+ case <-sigChan:
+ signal.Stop(sigChan)
+ close(sigChan)
+
+ err := server.Shutdown(context.Background())
+ if err != nil {
+ log.Fatal().Err(err).Msg("shutdown failed")
+ }
+ }
+}