Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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 @@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/gin-contrib/cors"
Expand All @@ -29,6 +30,28 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
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
}
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
}
host := ctx.Request.Host
if idx := strings.LastIndex(host, ":"); idx != -1 {
host = host[:idx]
}
if conf.Server.SSL.Port != 443 {
host = fmt.Sprintf("%s:%d", host, conf.Server.SSL.Port)
}
ctx.Redirect(http.StatusFound, fmt.Sprintf("https://%s%s", host, ctx.Request.RequestURI))
ctx.Abort()
})
}
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
Original file line number Diff line number Diff line change
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.