feat(docker): Add Dockerfile and compose file
This commit is contained in:
75
app/config/yaml.go
Normal file
75
app/config/yaml.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type YAMLConfig struct {
|
||||
Server Server `yaml:"server"`
|
||||
Database Database `yaml:"database"`
|
||||
Targets []Target `yaml:"targets"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Port int `yaml:"port" env:"SERVER_PORT"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Host string `yaml:"host" env:"DB_HOST"`
|
||||
Port string `yaml:"port" env:"DB_PORT"`
|
||||
User string `yaml:"user" env:"DB_USER"`
|
||||
Password string `yaml:"password" env:"DB_PASSWORD"`
|
||||
Database string `yaml:"database" env:"DB_DATABASE"`
|
||||
}
|
||||
|
||||
type Target struct {
|
||||
Name string `yaml:"name"`
|
||||
Group *string `yaml:"group"`
|
||||
Records []Record `yaml:"records"`
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Record string `yaml:"record"`
|
||||
Endpoints []Endpoint `yaml:"endpoints"`
|
||||
}
|
||||
|
||||
type Endpoint struct {
|
||||
Name string `yaml:"name"`
|
||||
Path string `yaml:"path"`
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
func LoadYAMLConfig(path string) (*YAMLConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config YAMLConfig
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (c *YAMLConfig) JSON() string {
|
||||
json, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(json)
|
||||
}
|
||||
|
||||
func (c *YAMLConfig) Map() *map[string]interface{} {
|
||||
jsonData, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var mapData map[string]interface{}
|
||||
if err := json.Unmarshal(jsonData, &mapData); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &mapData
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package database
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.secnex.io/secnex/gogwapi/config"
|
||||
"git.secnex.io/secnex/gogwapi/utils"
|
||||
|
||||
"git.secnex.io/secnex/masterlog"
|
||||
@@ -30,6 +31,16 @@ func NewDatabaseConfiguration(host, port, user, password, database string) *Data
|
||||
}
|
||||
}
|
||||
|
||||
func NewDatabaseConfigurationFromEnvAndConfig(config *config.YAMLConfig) *DatabaseConfiguration {
|
||||
return &DatabaseConfiguration{
|
||||
Host: config.Database.Host,
|
||||
Port: config.Database.Port,
|
||||
User: config.Database.User,
|
||||
Password: config.Database.Password,
|
||||
Database: config.Database.Database,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDatabaseConfigurationFromEnv() *DatabaseConfiguration {
|
||||
return &DatabaseConfiguration{
|
||||
Host: utils.GetEnv("DB_HOST", "localhost"),
|
||||
|
||||
@@ -4,6 +4,7 @@ go 1.25.3
|
||||
|
||||
require (
|
||||
git.secnex.io/secnex/masterlog v0.1.0
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
|
||||
@@ -3,6 +3,8 @@ git.secnex.io/secnex/masterlog v0.1.0/go.mod h1:OnDlwEzdkKMnqY+G5O9kHdhoJ6fH1llb
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
|
||||
48
app/main.go
48
app/main.go
@@ -1,30 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.secnex.io/secnex/gogwapi/config"
|
||||
"git.secnex.io/secnex/gogwapi/database"
|
||||
"git.secnex.io/secnex/gogwapi/models"
|
||||
"git.secnex.io/secnex/gogwapi/proxy"
|
||||
|
||||
"git.secnex.io/secnex/masterlog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pseudonymizer := masterlog.NewPseudonymizerFromString("1234567890")
|
||||
// pseudonymizer := masterlog.NewPseudonymizerFromEnv("MASTERLOG_SECRET")
|
||||
masterlog.SetPseudonymizer(pseudonymizer)
|
||||
masterlog.AddSensitiveFields("user_id", "password", "email")
|
||||
masterlog.SetLevel(masterlog.LevelInfo)
|
||||
masterlog.AddEncoder(&masterlog.JSONEncoder{})
|
||||
config, err := config.LoadYAMLConfig("../config.yaml")
|
||||
if err != nil {
|
||||
masterlog.Error("failed to load config", map[string]interface{}{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
targets := config.Targets
|
||||
masterlog.Info("Targets loaded", map[string]interface{}{"count": len(targets)})
|
||||
for _, target := range targets {
|
||||
targetDescription := target.Name
|
||||
if target.Group != nil {
|
||||
targetDescription = fmt.Sprintf("[%s] %s", *target.Group, target.Name)
|
||||
}
|
||||
masterlog.Info(targetDescription, map[string]interface{}{"target": target.Name, "group": *target.Group, "records": len(target.Records)})
|
||||
}
|
||||
|
||||
allModels := []interface{}{
|
||||
&models.Endpoint{},
|
||||
}
|
||||
|
||||
dbConfig := database.NewDatabaseConfigurationFromEnv()
|
||||
dbConfig := database.NewDatabaseConfigurationFromEnvAndConfig(config)
|
||||
masterlog.Info("Connecting to database", map[string]interface{}{"host": dbConfig.Host, "port": dbConfig.Port, "database": dbConfig.Database})
|
||||
if err := dbConfig.Connect(allModels...); err != nil {
|
||||
masterlog.Error("failed to connect to database", map[string]interface{}{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
masterlog.Info("Connected to database!")
|
||||
masterlog.Info("Starting server", map[string]interface{}{"port": ":3000"})
|
||||
|
||||
// Initialize reverse proxy
|
||||
reverseProxy := proxy.NewReverseProxy(config)
|
||||
|
||||
// Setup HTTP server
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", reverseProxy.ServeHTTP)
|
||||
|
||||
// Start server
|
||||
addr := fmt.Sprintf(":%d", config.Server.Port)
|
||||
masterlog.Info("Starting reverse proxy server", map[string]interface{}{
|
||||
"port": config.Server.Port,
|
||||
"address": addr,
|
||||
})
|
||||
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
masterlog.Error("server failed to start", map[string]interface{}{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
24
app/models/domain.go
Normal file
24
app/models/domain.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Verified bool `gorm:"not null;default:false" json:"verified"`
|
||||
VerificationToken string `gorm:"not null;unique" json:"verification_token"`
|
||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
|
||||
Records []Record `gorm:"foreignKey:DomainID" json:"-"`
|
||||
}
|
||||
|
||||
func (Domain) TableName() string {
|
||||
return "domains"
|
||||
}
|
||||
@@ -9,10 +9,15 @@ import (
|
||||
|
||||
type Endpoint struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
URL string `gorm:"not null" json:"url"`
|
||||
Path string `gorm:"not null;default:/" json:"path"`
|
||||
RecordID uuid.UUID `gorm:"not null;uniqueIndex:idx_endpoint_record_id" json:"record_id"`
|
||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
|
||||
Record Record `gorm:"foreignKey:RecordID" json:"-"`
|
||||
|
||||
EndpointGroups []EndpointGroup `gorm:"foreignKey:EndpointID" json:"-"`
|
||||
}
|
||||
|
||||
func (Endpoint) TableName() string {
|
||||
|
||||
24
app/models/endpoint_group.go
Normal file
24
app/models/endpoint_group.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EndpointGroup struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
GroupID uuid.UUID `gorm:"not null;uniqueIndex:idx_endpoint_group_group_id_endpoint_id" json:"group_id"`
|
||||
EndpointID uuid.UUID `gorm:"not null;uniqueIndex:idx_endpoint_group_group_id_endpoint_id" json:"endpoint_id"`
|
||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
|
||||
Group Group `gorm:"foreignKey:GroupID" json:"-"`
|
||||
Endpoint Endpoint `gorm:"foreignKey:EndpointID" json:"-"`
|
||||
}
|
||||
|
||||
func (EndpointGroup) TableName() string {
|
||||
return "endpoint_groups"
|
||||
}
|
||||
23
app/models/group.go
Normal file
23
app/models/group.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Description *string `json:"description"`
|
||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
|
||||
EndpointGroups []EndpointGroup `gorm:"foreignKey:GroupID" json:"-"`
|
||||
}
|
||||
|
||||
func (Group) TableName() string {
|
||||
return "groups"
|
||||
}
|
||||
25
app/models/record.go
Normal file
25
app/models/record.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
Record string `gorm:"not null;uniqueIndex:idx_record_domain_id" json:"record"`
|
||||
DomainID uuid.UUID `gorm:"not null;uniqueIndex:idx_record_domain_id" json:"domain_id"`
|
||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
|
||||
Domain Domain `gorm:"foreignKey:DomainID" json:"-"`
|
||||
|
||||
Endpoints []Endpoint `gorm:"foreignKey:RecordID" json:"-"`
|
||||
}
|
||||
|
||||
func (Record) TableName() string {
|
||||
return "records"
|
||||
}
|
||||
104
app/proxy/reverse.go
Normal file
104
app/proxy/reverse.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.secnex.io/secnex/gogwapi/config"
|
||||
"git.secnex.io/secnex/masterlog"
|
||||
)
|
||||
|
||||
type ReverseProxy struct {
|
||||
config *config.YAMLConfig
|
||||
}
|
||||
|
||||
func NewReverseProxy(cfg *config.YAMLConfig) *ReverseProxy {
|
||||
return &ReverseProxy{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
type TargetMatch struct {
|
||||
URL *url.URL
|
||||
Endpoint *config.Endpoint
|
||||
}
|
||||
|
||||
func (rp *ReverseProxy) findTarget(host string, path string) (*TargetMatch, error) {
|
||||
for _, target := range rp.config.Targets {
|
||||
for _, record := range target.Records {
|
||||
expectedHost := fmt.Sprintf("%s.%s", record.Record, target.Name)
|
||||
if host == expectedHost {
|
||||
for _, endpoint := range record.Endpoints {
|
||||
if endpoint.Path == "/" {
|
||||
targetURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TargetMatch{URL: targetURL, Endpoint: &endpoint}, nil
|
||||
}
|
||||
if strings.HasPrefix(path, endpoint.Path) {
|
||||
targetURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TargetMatch{URL: targetURL, Endpoint: &endpoint}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no target found for host: %s, path: %s", host, path)
|
||||
}
|
||||
|
||||
func (rp *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Host
|
||||
path := r.URL.Path
|
||||
|
||||
targetMatch, err := rp.findTarget(host, path)
|
||||
if err != nil {
|
||||
masterlog.Error("target not found", map[string]interface{}{
|
||||
"host": host,
|
||||
"path": path,
|
||||
"error": err.Error(),
|
||||
})
|
||||
http.Error(w, "Service not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
targetURL := targetMatch.URL
|
||||
endpoint := targetMatch.Endpoint
|
||||
|
||||
// Transform the path: remove the endpoint prefix if it's not "/"
|
||||
var transformedPath string
|
||||
if endpoint.Path == "/" {
|
||||
transformedPath = path
|
||||
} else {
|
||||
transformedPath = strings.TrimPrefix(path, endpoint.Path)
|
||||
if transformedPath == "" {
|
||||
transformedPath = "/"
|
||||
}
|
||||
}
|
||||
|
||||
masterlog.Info("proxying request", map[string]interface{}{
|
||||
"host": host,
|
||||
"original_path": path,
|
||||
"transformed_path": transformedPath,
|
||||
"endpoint_prefix": endpoint.Path,
|
||||
"target": targetURL.String(),
|
||||
})
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(targetURL)
|
||||
|
||||
// Create a new request with the transformed path
|
||||
r.URL.Host = targetURL.Host
|
||||
r.URL.Scheme = targetURL.Scheme
|
||||
r.URL.Path = transformedPath
|
||||
r.Header.Set("X-Forwarded-Host", host)
|
||||
r.Header.Set("X-Forwarded-For", r.RemoteAddr)
|
||||
r.Header.Set("X-Forwarded-Path", path) // Original path for debugging
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
41
app/repositories/domain.go
Normal file
41
app/repositories/domain.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"git.secnex.io/secnex/gogwapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DomainRepository interface {
|
||||
Create(domain *models.Domain) error
|
||||
Get(id string) (*models.Domain, error)
|
||||
Update(domain *models.Domain) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type domainRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDomainRepository(db *gorm.DB) DomainRepository {
|
||||
return &domainRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *domainRepository) Create(domain *models.Domain) error {
|
||||
return r.db.Create(domain).Error
|
||||
}
|
||||
|
||||
func (r *domainRepository) Get(name string) (*models.Domain, error) {
|
||||
var domain models.Domain
|
||||
if err := r.db.Where("name = ?", name).First(&domain).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain, nil
|
||||
}
|
||||
|
||||
func (r *domainRepository) Update(domain *models.Domain) error {
|
||||
return r.db.Save(domain).Error
|
||||
}
|
||||
|
||||
func (r *domainRepository) Delete(id string) error {
|
||||
return r.db.Delete(&models.Domain{}, id).Error
|
||||
}
|
||||
5
app/schema/domain.go
Normal file
5
app/schema/domain.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package schema
|
||||
|
||||
type DomainSchema struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
Reference in New Issue
Block a user