Skip to content

Commit

Permalink
Merge pull request #16 from hvuhsg/development
Browse files Browse the repository at this point in the history
Better structure
  • Loading branch information
hvuhsg authored Oct 22, 2024
2 parents fe71258 + c530f23 commit d8da24d
Show file tree
Hide file tree
Showing 10 changed files with 526 additions and 331 deletions.
62 changes: 59 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,44 @@ func (s Service) validate() error {
}

type TLS struct {
KeyFile *string `yaml:"keyfile"`
CertFile *string `yaml:"certfile"`
Auto bool `yaml:"auto"`
Domains []string `yaml:"domain"`
Email *string `yaml:"email"`
KeyFile *string `yaml:"keyfile"`
CertFile *string `yaml:"certfile"`
}

func (tls TLS) validate() error {
if tls.Auto {
if len(tls.Domains) == 0 {
return errors.New("when using the auto tls feature you MUST include a list of domains to issue certificates for")
}
if tls.Email == nil || len(*tls.Email) == 0 || !isValidEmail(*tls.Email) {
return errors.New("when using the auto tls feature you MUST include a valid email for the lets-encrypt registration")
}
}

if tls.CertFile != nil {
if tls.KeyFile == nil {
return errors.New("you MUST provide certfile AND keyfile")
}
}

if tls.KeyFile != nil {
if tls.CertFile == nil {
return errors.New("you MUST provide certfile AND keyfile")
}

if !isValidFile(*tls.CertFile) {
return errors.New("certfile path is invalid")
}

if !isValidFile(*tls.KeyFile) {
return errors.New("keyfile path is invalid")
}
}

return nil
}

type OTEL struct {
Expand Down Expand Up @@ -217,7 +253,7 @@ type Config struct {
OTEL *OTEL `yaml:"open_telemetry"`

// TLS options
SSL TLS `yaml:"ssl"`
TLS TLS `yaml:"ssl"`

Services []Service `yaml:"services"`
}
Expand Down Expand Up @@ -247,6 +283,18 @@ func (c Config) Validate(currentVersion string) error {
}
}

if c.Port == 0 {
return errors.New("port is required")
}

if err := c.TLS.validate(); err != nil {
return err
}

if c.TLS.Auto && c.Port != 443 {
return errors.New("the auto tls feature is only available if the server runs on port 443")
}

for _, service := range c.Services {
if err := service.validate(); err != nil {
return err
Expand Down Expand Up @@ -406,3 +454,11 @@ func isValidGRPCAddress(address string) error {

return nil
}

func isValidEmail(email string) bool {
// Define a regular expression for valid email addresses
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

// Match the email string with the regular expression
return emailRegex.MatchString(email)
}
3 changes: 2 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ func TestConfigValidate(t *testing.T) {
currentVersion string
wantErr bool
}{
{"Valid config", Config{Version: "1.0.0", Host: "localhost", Services: []Service{{Domain: "example.com", Paths: []Path{{Path: "/api", Destination: ptr("http://api.example.com")}}}}}, "1.0.0", false},
{"Valid config", Config{Version: "1.0.0", Host: "localhost", Port: 80, Services: []Service{{Domain: "example.com", Paths: []Path{{Path: "/api", Destination: ptr("http://api.example.com")}}}}}, "1.0.0", false},
{"AutoTLS with port != 443", Config{Version: "1.0.0", Host: "localhost", Port: 80, TLS: TLS{Auto: true, Domains: []string{"example.com"}}, Services: []Service{{Domain: "example.com", Paths: []Path{{Path: "/api", Destination: ptr("http://api.example.com")}}}}}, "1.0.0", true},
{"Missing version", Config{Host: "localhost"}, "1.0.0", true},
{"Invalid version", Config{Version: "invalid", Host: "localhost"}, "1.0.0", true},
{"Future version", Config{Version: "2.0.0", Host: "localhost"}, "1.0.0", true},
Expand Down
103 changes: 2 additions & 101 deletions gatego.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ package gatego

import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"time"

"github.com/hvuhsg/gatego/config"
"github.com/hvuhsg/gatego/contextvalues"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

const serviceName = "gatego"
Expand Down Expand Up @@ -48,15 +42,13 @@ func (gg GateGo) Run() error {
checker := createChecker(gg.config.Services)
checker.Start()

table, err := NewHandlersTable(gg.ctx, useOtel, gg.config.Services)
server, err := newServer(gg.ctx, gg.config, useOtel)
if err != nil {
return err
}

server := gg.createServer(table)
defer server.Shutdown(gg.ctx)

serveErrChan, err := serve(server, gg.config.SSL.CertFile, gg.config.SSL.KeyFile)
serveErrChan, err := server.serve(gg.config.TLS.CertFile, gg.config.TLS.KeyFile)
if err != nil {
return err
}
Expand Down Expand Up @@ -93,94 +85,3 @@ func createChecker(services []config.Service) *Checker {

return checker
}

func (gg GateGo) createServer(table HandlerTable) *http.Server {
mux := http.NewServeMux()

// handleFunc is a replacement for mux.HandleFunc
// which enriches the handler's HTTP instrumentation with the pattern as the http.route.
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
// Configure the "http.route" for the HTTP instrumentation.
handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
mux.Handle(pattern, handler)
}

handleFunc("/", func(w http.ResponseWriter, r *http.Request) {
handler := table.GetHandler(r.Host, r.URL.Path)

if handler == nil {
w.WriteHeader(http.StatusNotFound)
return
}

handler.ServeHTTP(w, r)
})

// Add HTTP instrumentation for the whole server.
handler := otelhttp.NewHandler(mux, "/")

addr := fmt.Sprintf("%s:%d", gg.config.Host, gg.config.Port)

// Start HTTP server.
server := &http.Server{
Addr: addr,
BaseContext: func(_ net.Listener) context.Context { return gg.ctx },
ReadTimeout: time.Second,
WriteTimeout: 10 * time.Second,
Handler: handler,
}

return server
}

func serve(server *http.Server, certfile *string, keyfile *string) (chan error, error) {
supportTLS, err := checkTLSConfig(certfile, keyfile)
if err != nil {
return nil, err
}

serveErr := make(chan error, 1)

go func() {
if supportTLS {
log.Default().Printf("Serving proxy with TLS %s\n", server.Addr)
serveErr <- server.ListenAndServeTLS(*certfile, *keyfile)
} else {
log.Default().Printf("Serving proxy %s\n", server.Addr)
serveErr <- server.ListenAndServe()
}
}()

return serveErr, nil
}

func checkTLSConfig(certfile *string, keyfile *string) (bool, error) {
if keyfile == nil || certfile == nil || *keyfile == "" || *certfile == "" {
return false, nil
}

if !fileExists(*keyfile) {
return false, fmt.Errorf("can't find keyfile at '%s'", *keyfile)
}

if !fileExists(*certfile) {
return false, fmt.Errorf("can't find certfile at '%s'", *certfile)
}

return true, nil
}

func fileExists(filepath string) bool {
_, err := os.Stat(filepath)

if os.IsNotExist(err) {
return false
}

// If we cant check the file info we probably can't open the file
if err != nil {
return false
}

return true
}
152 changes: 152 additions & 0 deletions handlers/files_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package handlers

import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)

func TestRemoveBaseURLPath(t *testing.T) {
tests := []struct {
name string
basePath string
fullPath string
want string
wantErr bool
}{
{
name: "simple path",
basePath: "/api",
fullPath: "/api/file.txt",
want: "/file.txt",
wantErr: false,
},
{
name: "path with multiple segments",
basePath: "/api/v1",
fullPath: "/api/v1/docs/file.txt",
want: "/docs/file.txt",
wantErr: false,
},
{
name: "paths with trailing slashes",
basePath: "/api/",
fullPath: "/api/file.txt/",
want: "/file.txt",
wantErr: false,
},
{
name: "paths without leading slashes",
basePath: "api",
fullPath: "api/file.txt",
want: "/file.txt",
wantErr: false,
},
{
name: "path not in base path",
basePath: "/api",
fullPath: "/other/file.txt",
want: "",
wantErr: true,
},
{
name: "empty paths",
basePath: "",
fullPath: "/file.txt",
want: "/file.txt",
wantErr: false,
},
{
name: "identical paths",
basePath: "/api",
fullPath: "/api",
want: "/",
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := removeBaseURLPath(tt.basePath, tt.fullPath)
if (err != nil) != tt.wantErr {
t.Errorf("removeBaseURLPath() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("removeBaseURLPath() = %v, want %v", got, tt.want)
}
})
}
}

func TestFiles_ServeHTTP(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "files_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)

// Create a test file
testContent := []byte("test file content")
testFilePath := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFilePath, testContent, 0644); err != nil {
t.Fatal(err)
}

tests := []struct {
name string
basePath string
requestPath string
expectedStatus int
expectedBody string
}{
{
name: "valid file request",
basePath: "/files",
requestPath: "/files/test.txt",
expectedStatus: http.StatusOK,
expectedBody: "test file content",
},
{
name: "file not found",
basePath: "/files",
requestPath: "/files/nonexistent.txt",
expectedStatus: http.StatusNotFound,
expectedBody: "404 page not found\n",
},
{
name: "path outside base path",
basePath: "/files",
requestPath: "/other/test.txt",
expectedStatus: http.StatusNotFound,
expectedBody: "404 page not found\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a new Files handler
files := NewFiles(tmpDir, tt.basePath)

// Create a test request
req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
w := httptest.NewRecorder()

// Serve the request
files.ServeHTTP(w, req)

// Check status code
if w.Code != tt.expectedStatus {
t.Errorf("ServeHTTP() status = %v, want %v", w.Code, tt.expectedStatus)
}

// Check response body
if w.Body.String() != tt.expectedBody {
t.Errorf("ServeHTTP() body = %v, want %v", w.Body.String(), tt.expectedBody)
}
})
}
}
Loading

0 comments on commit d8da24d

Please sign in to comment.