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

Unix socket implementation #621

Merged
merged 2 commits into from Jan 20, 2024
Merged
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
Expand Up @@ -28,7 +28,7 @@ jobs:
- run: make build-js
- uses: golangci/golangci-lint-action@v3
with:
version: v1.53
version: v1.55
args: --timeout=5m
skip-cache: true
- run: go mod download
Expand Down
5 changes: 4 additions & 1 deletion app.go
Expand Up @@ -51,5 +51,8 @@ func main() {
engine, closeable := router.Create(db, vInfo, conf)
defer closeable()

runner.Run(engine, conf)
if err := runner.Run(engine, conf); err != nil {
fmt.Println("Server error: ", err)
os.Exit(1)
}
}
4 changes: 2 additions & 2 deletions config.example.yml
Expand Up @@ -3,13 +3,13 @@

server:
keepaliveperiodseconds: 0 # 0 = use Go default (15s); -1 = disable keepalive; set the interval in which keepalive packets will be sent. Only change this value if you know what you are doing.
listenaddr: "" # the address to bind on, leave empty to bind on all addresses
listenaddr: "" # the address to bind on, leave empty to bind on all addresses. Prefix with "unix:" to create a unix socket. Example: "unix:/tmp/gotify.sock".
port: 80 # the port the HTTP server will listen on

ssl:
enabled: false # if https should be enabled
redirecttohttps: true # redirect to https if site is accessed by http
listenaddr: "" # the address to bind on, leave empty to bind on all addresses
listenaddr: "" # the address to bind on, leave empty to bind on all addresses. Prefix with "unix:" to create a unix socket. Example: "unix:/tmp/gotify.sock".
port: 443 # the https port
certfile: # the cert file (leave empty when using letsencrypt)
certkey: # the cert key (leave empty when using letsencrypt)
Expand Down
23 changes: 23 additions & 0 deletions router/router.go
Expand Up @@ -5,6 +5,7 @@
"net/http"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/gin-contrib/cors"
Expand All @@ -29,6 +30,28 @@
g.Use(gin.LoggerWithFormatter(logFormatter), gin.Recovery(), gerror.Handler(), location.Default())
g.NoRoute(gerror.NotFound())

if conf.Server.SSL.Enabled != nil && conf.Server.SSL.RedirectToHTTPS != nil && *conf.Server.SSL.Enabled && *conf.Server.SSL.RedirectToHTTPS {
g.Use(func(ctx *gin.Context) {
if ctx.Request.TLS != nil {
ctx.Next()
return

Check warning on line 37 in router/router.go

View check run for this annotation

Codecov / codecov/patch

router/router.go#L34-L37

Added lines #L34 - L37 were not covered by tests
}
if ctx.Request.Method != http.MethodGet && ctx.Request.Method != http.MethodHead {
ctx.Data(http.StatusBadRequest, "text/plain; charset=utf-8", []byte("Use HTTPS"))
ctx.Abort()
return

Check warning on line 42 in router/router.go

View check run for this annotation

Codecov / codecov/patch

router/router.go#L39-L42

Added lines #L39 - L42 were not covered by tests
}
host := ctx.Request.Host
if idx := strings.LastIndex(host, ":"); idx != -1 {
host = host[:idx]

Check warning on line 46 in router/router.go

View check run for this annotation

Codecov / codecov/patch

router/router.go#L44-L46

Added lines #L44 - L46 were not covered by tests
}
if conf.Server.SSL.Port != 443 {
host = fmt.Sprintf("%s:%d", host, conf.Server.SSL.Port)

Check warning on line 49 in router/router.go

View check run for this annotation

Codecov / codecov/patch

router/router.go#L48-L49

Added lines #L48 - L49 were not covered by tests
}
ctx.Redirect(http.StatusFound, fmt.Sprintf("https://%s%s", host, ctx.Request.RequestURI))
ctx.Abort()

Check warning on line 52 in router/router.go

View check run for this annotation

Codecov / codecov/patch

router/router.go#L51-L52

Added lines #L51 - L52 were not covered by tests
})
}
streamHandler := stream.New(
time.Duration(conf.Server.Stream.PingPeriodSeconds)*time.Second, 15*time.Second, conf.Server.Stream.AllowedOrigins)
go func() {
Expand Down
114 changes: 65 additions & 49 deletions runner/runner.go
Expand Up @@ -4,83 +4,99 @@ import (
"context"
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"strconv"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/gotify/server/v2/config"
"golang.org/x/crypto/acme/autocert"
)

// Run starts the http server and if configured a https server.
func Run(router http.Handler, conf *config.Configuration) {
httpHandler := router
func Run(router http.Handler, conf *config.Configuration) error {
shutdown := make(chan error)
go doShutdownOnSignal(shutdown)

httpListener, err := startListening("plain connection", conf.Server.ListenAddr, conf.Server.Port, conf.Server.KeepAlivePeriodSeconds)
if err != nil {
return err
}
defer httpListener.Close()

s := &http.Server{Handler: router}
if *conf.Server.SSL.Enabled {
if *conf.Server.SSL.RedirectToHTTPS {
httpHandler = redirectToHTTPS(strconv.Itoa(conf.Server.SSL.Port))
if *conf.Server.SSL.LetsEncrypt.Enabled {
applyLetsEncrypt(s, conf)
}

addr := fmt.Sprintf("%s:%d", conf.Server.SSL.ListenAddr, conf.Server.SSL.Port)
s := &http.Server{
Addr: addr,
Handler: router,
httpsListener, err := startListening("TLS connection", conf.Server.SSL.ListenAddr, conf.Server.SSL.Port, conf.Server.KeepAlivePeriodSeconds)
if err != nil {
return err
}
defer httpsListener.Close()

if *conf.Server.SSL.LetsEncrypt.Enabled {
certManager := autocert.Manager{
Prompt: func(tosURL string) bool { return *conf.Server.SSL.LetsEncrypt.AcceptTOS },
HostPolicy: autocert.HostWhitelist(conf.Server.SSL.LetsEncrypt.Hosts...),
Cache: autocert.DirCache(conf.Server.SSL.LetsEncrypt.Cache),
}
httpHandler = certManager.HTTPHandler(httpHandler)
s.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate}
}
fmt.Println("Started Listening for TLS connection on " + addr)
go func() {
listener := startListening(addr, conf.Server.KeepAlivePeriodSeconds)
log.Fatal(s.ServeTLS(listener, conf.Server.SSL.CertFile, conf.Server.SSL.CertKey))
err := s.ServeTLS(httpsListener, conf.Server.SSL.CertFile, conf.Server.SSL.CertKey)
doShutdown(shutdown, err)
}()
}
addr := fmt.Sprintf("%s:%d", conf.Server.ListenAddr, conf.Server.Port)
fmt.Println("Started Listening for plain HTTP connection on " + addr)
server := &http.Server{Addr: addr, Handler: httpHandler}
go func() {
err := s.Serve(httpListener)
doShutdown(shutdown, err)
}()

err = <-shutdown
fmt.Println("Shutting down:", err)

log.Fatal(server.Serve(startListening(addr, conf.Server.KeepAlivePeriodSeconds)))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return s.Shutdown(ctx)
}

func startListening(addr string, keepAlive int) net.Listener {
lc := net.ListenConfig{KeepAlive: time.Duration(keepAlive) * time.Second}
conn, err := lc.Listen(context.Background(), "tcp", addr)
if err != nil {
log.Fatalln("Could not listen on", addr, err)
func doShutdownOnSignal(shutdown chan<- error) {
onSignal := make(chan os.Signal, 1)
signal.Notify(onSignal, os.Interrupt, syscall.SIGTERM)
sig := <-onSignal
doShutdown(shutdown, fmt.Errorf("received signal %s", sig))
}

func doShutdown(shutdown chan<- error, err error) {
select {
case shutdown <- err:
default:
// If there is no one listening on the shutdown channel, then the
// shutdown is already initiated and we can ignore these errors.
}
return conn
}

func redirectToHTTPS(port string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "Use HTTPS", http.StatusBadRequest)
return
}
func startListening(connectionType, listenAddr string, port, keepAlive int) (net.Listener, error) {
network, addr := getNetworkAndAddr(listenAddr, port)
lc := net.ListenConfig{KeepAlive: time.Duration(keepAlive) * time.Second}

target := "https://" + changePort(r.Host, port) + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusFound)
l, err := lc.Listen(context.Background(), network, addr)
if err == nil {
fmt.Println("Started listening for", connectionType, "on", l.Addr().Network(), l.Addr().String())
}
return l, err
}

func changePort(hostPort, port string) string {
host, _, err := net.SplitHostPort(hostPort)
if err != nil {
// There is no exported error.
if !strings.Contains(err.Error(), "missing port") {
return hostPort
}
host = hostPort
func getNetworkAndAddr(listenAddr string, port int) (string, string) {
if strings.HasPrefix(listenAddr, "unix:") {
return "unix", strings.TrimPrefix(listenAddr, "unix:")
}
return "tcp", fmt.Sprintf("%s:%d", listenAddr, port)
}

func applyLetsEncrypt(s *http.Server, conf *config.Configuration) {
certManager := autocert.Manager{
Prompt: func(tosURL string) bool { return *conf.Server.SSL.LetsEncrypt.AcceptTOS },
HostPolicy: autocert.HostWhitelist(conf.Server.SSL.LetsEncrypt.Hosts...),
Cache: autocert.DirCache(conf.Server.SSL.LetsEncrypt.Cache),
}
return net.JoinHostPort(host, port)
s.Handler = certManager.HTTPHandler(s.Handler)
s.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate}
}
35 changes: 0 additions & 35 deletions runner/runner_test.go

This file was deleted.