Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions cmd/serverNameExample_httpExample/initial/createService.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package initial

import (
"strconv"

"github.com/go-dev-frame/sponge/internal/config"
"github.com/go-dev-frame/sponge/internal/server"

Expand All @@ -15,8 +13,7 @@
var servers []app.IServer

// create a http service
httpAddr := ":" + strconv.Itoa(cfg.HTTP.Port)
httpServer := server.NewHTTPServer(httpAddr,
httpServer := server.NewHTTPServer(cfg.HTTP,

Check failure on line 16 in cmd/serverNameExample_httpExample/initial/createService.go

View workflow job for this annotation

GitHub Actions / Golangci-lint

cannot use cfg.HTTP (variable of type config.HTTP) as string value in argument to server.NewHTTPServer) (typecheck)

Check failure on line 16 in cmd/serverNameExample_httpExample/initial/createService.go

View workflow job for this annotation

GitHub Actions / Golangci-lint

cannot use cfg.HTTP (variable of type config.HTTP) as string value in argument to server.NewHTTPServer (typecheck)
server.WithHTTPIsProd(cfg.App.Env == "prod"),
)
servers = append(servers, httpServer)
Expand Down
4 changes: 4 additions & 0 deletions cmd/sponge/commands/generate/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,10 @@ func getHTTPServiceFields() []replacer.Field {
Old: appConfigFileMark3,
New: "",
},
{
Old: "http_test.go.noregistry",
New: "http_test.go",
},
{
Old: "http.go.noregistry",
New: "http.go",
Expand Down
2 changes: 1 addition & 1 deletion cmd/sponge/commands/generate/http-pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (g *httpPbGenerator) generateCode() (string, error) {
"routers_pbExample.go",
},
"internal/server": {
"http.go.noregistry", "http_option.go.noregistry",
"http.go.noregistry", "http_option.go.noregistry", "http_test.go.noregistry",
},
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/sponge/commands/generate/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (g *httpGenerator) generateCode() (string, error) {
"routers.go", "userExample.go",
},
"internal/server": {
"http.go.noregistry", "http_option.go.noregistry",
"http.go.noregistry", "http_option.go.noregistry", "http_test.go.noregistry",
},
"internal/types": {
"swagger_types.go", "userExample_types.go",
Expand Down
14 changes: 13 additions & 1 deletion cmd/sponge/commands/generate/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,19 @@ func NewCenter(configFile string) (*Center, error) {
httpServerConfigCode = `# http server settings
http:
port: 8080 # listen port
timeout: 0 # request timeout, unit(second), if 0 means not set, if greater than 0 means set timeout, if enableHTTPProfile is true, it needs to set 0 or greater than 60s`
httpsPort: 8443 # https listen port when tls is enabled
timeout: 0 # request timeout, unit(second), if 0 means not set, if greater than 0 means set timeout, if enableHTTPProfile is true, it needs to set 0 or greater than 60s
idleTimeout: 60 # http idle timeout, unit(second)
readTimeout: 30 # http read timeout, unit(second)
writeTimeout: 30 # http write timeout, unit(second)
tls:
domains:
- "" # list of domains for automatic tls certificates, empty disables tls
acmeDirectory: "https://acme-v02.api.letsencrypt.org/directory" # acme directory url
storagePath: "./storage/autocert" # directory to cache certificates
eab:
kid: "" # external account binding key identifier
hmacKey: "" # base64url encoded external account binding hmac key`

rpcServerConfigCode = `# grpc server settings
grpc:
Expand Down
14 changes: 13 additions & 1 deletion configs/serverNameExample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,19 @@ app:
# http server settings
http:
port: 8080 # listen port
timeout: 0 # request timeout, unit(second), if 0 means not set, if greater than 0 means set timeout, if enableHTTPProfile is true, it needs to set 0 or greater than 60s
httpsPort: 8443 # https listen port when tls is enabled
timeout: 0 # request timeout, unit(second), if 0 means not set, if greater than 0 means set timeout, if enableHTTPProfile is true, it needs to set 0 or greater than 60s
idleTimeout: 60 # http idle timeout, unit(second)
readTimeout: 30 # http read timeout, unit(second)
writeTimeout: 30 # http write timeout, unit(second)
tls:
domains:
- "" # list of domains for automatic tls certificates, empty disables tls
acmeDirectory: "https://acme-v02.api.letsencrypt.org/directory" # acme directory url
storagePath: "./storage/autocert" # directory to cache certificates
eab:
kid: "" # external account binding key identifier
hmacKey: "" # base64url encoded external account binding hmac key


# grpc server settings
Expand Down
134 changes: 116 additions & 18 deletions internal/server/http.go.noregistry
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,102 @@ package server

import (
"context"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"

"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"

"github.com/go-dev-frame/sponge/pkg/app"
"github.com/go-dev-frame/sponge/pkg/logger"

"github.com/go-dev-frame/sponge/internal/config"
"github.com/go-dev-frame/sponge/internal/routers"
)

var _ app.IServer = (*httpServer)(nil)

type httpServer struct {
addr string
server *http.Server
httpAddr string
httpsAddr string
httpServer *http.Server
httpsServer *http.Server
tlsEnabled bool
}

// Start http service
// Start http/https service
func (s *httpServer) Start() error {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("listen server error: %v", err)
if s.tlsEnabled {
errCh := make(chan error, 2)

go func() {
errCh <- listenAndServe(s.httpServer)
}()

go func() {
errCh <- listenAndServeTLS(s.httpsServer)
}()

var firstErr error
for i := 0; i < 2; i++ {
if err := <-errCh; err != nil {
if firstErr == nil {
firstErr = err
} else {
logger.Error("http server encountered multiple errors", logger.Err(err))
}
}
}

return firstErr
}

if err := listenAndServe(s.httpServer); err != nil {
return err
}

return nil
}

// Stop http service
// Stop http/https service
func (s *httpServer) Stop() error {
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
return s.server.Shutdown(ctx)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var firstErr error
if err := s.httpServer.Shutdown(ctx); err != nil {
firstErr = err
}

if s.tlsEnabled && s.httpsServer != nil {
if err := s.httpsServer.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) {
if firstErr == nil {
firstErr = err
} else {
logger.Error("https server shutdown reported additional error", logger.Err(err))
}
}
}

return firstErr
}

// String comment
// String provides a human readable description of listener addresses.
func (s *httpServer) String() string {
return "http service address " + s.addr
if s.tlsEnabled {
return fmt.Sprintf("http service redirecting on %s and https service address %s", s.httpAddr, s.httpsAddr)
}
return "http service address " + s.httpAddr
}

// NewHTTPServer creates a new http server
func NewHTTPServer(addr string, opts ...HTTPOption) app.IServer {
// NewHTTPServer creates an HTTP server with optional automatic TLS.
func NewHTTPServer(cfg config.HTTP, opts ...HTTPOption) app.IServer {
o := defaultHTTPOptions()
o.apply(opts...)

Expand All @@ -50,16 +107,57 @@ func NewHTTPServer(addr string, opts ...HTTPOption) app.IServer {
gin.SetMode(gin.DebugMode)
}

router := routers.NewRouter()
server := &http.Server{
Addr: addr,
Handler: router,
appHandler := o.handler
if appHandler == nil {
appHandler = routers.NewRouter()
}

readTimeout := secondsToDuration(cfg.ReadTimeout)
writeTimeout := secondsToDuration(cfg.WriteTimeout)
idleTimeout := secondsToDuration(cfg.IdleTimeout)

httpSrv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: appHandler,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
MaxHeaderBytes: 1 << 20,
}

domains := filterDomains(cfg.TLS.Domains)
tlsEnabled := len(domains) > 0

var (
httpsSrv *http.Server
httpsAddr string
)
if tlsEnabled {
manager := buildAutocertManager(cfg, domains)
httpSrv.Handler = manager.HTTPHandler(http.HandlerFunc(httpRedirectHandler))

httpsSrv = &http.Server{
Addr: fmt.Sprintf(":%d", cfg.HTTPSPort),
Handler: appHandler,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
MaxHeaderBytes: 1 << 20,
TLSConfig: manager.TLSConfig(),
}
httpsAddr = httpsSrv.Addr

logger.Info("automatic TLS enabled", logger.String("http_addr", httpSrv.Addr), logger.String("https_addr", httpsSrv.Addr), logger.Any("domains", domains))
} else {
logger.Info("automatic TLS disabled", logger.String("http_addr", httpSrv.Addr))
}

return &httpServer{
addr: addr,
server: server,
httpAddr: httpSrv.Addr,
httpsAddr: httpsAddr,
httpServer: httpSrv,
httpsServer: httpsSrv,
tlsEnabled: tlsEnabled,
}
}

Expand Down
15 changes: 13 additions & 2 deletions internal/server/http_option.go.noregistry
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package server

import "net/http"

// HTTPOption setting up http
type HTTPOption func(*httpOptions)

type httpOptions struct {
isProd bool
isProd bool
handler http.Handler
}

func defaultHTTPOptions() *httpOptions {
return &httpOptions{
isProd: false,
isProd: false,
handler: nil,
}
}

Expand All @@ -25,3 +29,10 @@ func WithHTTPIsProd(isProd bool) HTTPOption {
o.isProd = isProd
}
}

// WithHTTPHandler allows injecting a custom http handler (primarily for testing)
func WithHTTPHandler(handler http.Handler) HTTPOption {
return func(o *httpOptions) {
o.handler = handler
}
}
87 changes: 87 additions & 0 deletions internal/server/http_test.go.noregistry
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package server

import (
"encoding/base64"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/require"
"golang.org/x/crypto/acme"

"github.com/go-dev-frame/sponge/internal/config"
)

func setTestConfig(t *testing.T, httpCfg config.HTTP) {
t.Helper()
config.Set(&config.Config{
App: config.App{
Env: "dev",
},
HTTP: httpCfg,
})
t.Cleanup(func() {
config.Set(nil)
})
}

func TestSecondsToDuration(t *testing.T) {
require.Equal(t, time.Duration(0), secondsToDuration(0))
require.Equal(t, time.Duration(0), secondsToDuration(-1))
require.Equal(t, 30*time.Second, secondsToDuration(30))
}

func TestFilterDomains(t *testing.T) {
input := []string{"example.com", "", " other.example.com "}
result := filterDomains(input)
require.Equal(t, []string{"example.com", "other.example.com"}, result)
}

func TestExternalAccountBinding(t *testing.T) {
// returns nil when kid or key missing
require.Nil(t, externalAccountBinding(config.Eab{}))

key := []byte("secret")
encoded := base64.RawURLEncoding.EncodeToString(key)
binding := externalAccountBinding(config.Eab{Kid: "kid", HmacKey: encoded})
require.NotNil(t, binding)
require.Equal(t, "kid", binding.KID)
require.Equal(t, key, binding.Key)
}

func TestNewHTTPServer_TLSEnabled(t *testing.T) {
httpCfg := config.HTTP{
Port: 8080,
HTTPSPort: 8443,
IdleTimeout: 10,
ReadTimeout: 5,
WriteTimeout: 5,
TLS: config.TLS{
Domains: []string{"example.com"},
AcmeDirectory: acme.LetsEncryptURL,
StoragePath: t.TempDir(),
},
}

setTestConfig(t, httpCfg)
server := NewHTTPServer(httpCfg, WithHTTPHandler(http.NewServeMux())).(*httpServer)
require.True(t, server.tlsEnabled)
require.NotNil(t, server.httpsServer)
require.Equal(t, ":8080", server.httpAddr)
require.Equal(t, ":8443", server.httpsServer.Addr)
}

func TestNewHTTPServer_TLSDisabledWhenNoDomains(t *testing.T) {
httpCfg := config.HTTP{
Port: 8080,
HTTPSPort: 8443,
TLS: config.TLS{
Domains: []string{"", " "},
},
}

setTestConfig(t, httpCfg)
server := NewHTTPServer(httpCfg, WithHTTPHandler(http.NewServeMux())).(*httpServer)
require.False(t, server.tlsEnabled)
require.Nil(t, server.httpsServer)
}
Loading