commit 407a8d5052127b9cdd27ca9da21f9520cf4676f2 Author: pk Date: Wed Aug 28 12:31:17 2024 +0200 First version diff --git a/cmd/hoster_cli/hoster.go b/cmd/hoster_cli/hoster.go new file mode 100644 index 0000000..1636073 --- /dev/null +++ b/cmd/hoster_cli/hoster.go @@ -0,0 +1,140 @@ +package main + +import ( + "fmt" + "hoster/internals/conf" + "hoster/internals/helpers" + "hoster/internals/templates" + "hoster/internals/types" + "os" + "path/filepath" + + "github.com/gookit/goutil/fsutil" + "github.com/gookit/goutil/sysutil/cmdr" + "github.com/kataras/golog" + "github.com/urfave/cli/v2" + "golang.org/x/exp/slices" +) + +func main() { + conf.LoadConfig("config.yml") + + app := &cli.App{ + Name: "hoster", + Usage: "Hoster", + Commands: []*cli.Command{ + { + Name: "new", + Usage: "Setup a new hosting project.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "The name of the project", + Required: true, + }, + &cli.StringFlag{ + Name: "domain", + Aliases: []string{"d"}, + Usage: "The description of the project", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + name := c.String("name") + domain := c.String("domain") + projectRoot := conf.App.MustString("projects.root") + sitesAvail := conf.App.MustString("nginx.sitesAvailable") + sitesEnabled := conf.App.MustString("nginx.sitesEnabled") + nginxFile := filepath.Join(sitesAvail, name) + + projectFolder := filepath.Join(projectRoot, name) + + existingProjects, err := helpers.FindHostConfigs(sitesEnabled) + if err != nil { + return err + } + + usedPorts := []int{} + for _, project := range existingProjects { + usedPorts = append(usedPorts, project.InternalPort) + } + + port := conf.App.Int("nginx.portRangeStart") + freePort := 0 + for port <= conf.App.Int("nginx.portRangeEnd") { + if !slices.Contains(usedPorts, port) { + freePort = port + break + } + port++ + } + + if freePort == 0 { + return fmt.Errorf("no free port available") + } + + if !fsutil.IsDir(projectFolder) { + golog.Infof("Creating project folder '%s'", projectFolder) + err := os.MkdirAll(projectFolder, 0755) + if err != nil { + return err + } + } + + if fsutil.IsFile(nginxFile) { + return fmt.Errorf("the nginx config file '%s' already exists", nginxFile) + } + + // Set owner and group of project folder + err = os.Chown(projectFolder, conf.App.Int("projects.owner"), conf.App.Int("projects.group")) + if err != nil { + return err + } + + project := types.Project{ + Name: name, + Domain: domain, + InternalPort: freePort, + } + + // Create nginx config file + err = templates.Execute("nginx", nginxFile, project) + + if err != nil { + return err + } + + // Symlink nginx config file + err = os.Symlink(nginxFile, filepath.Join(sitesEnabled, name)) + if err != nil { + return err + } + + // Execute certbot command: "systemctl stop nginx && sudo certbot certonly --standalone --preferred-challenges http -d {domain} && sudo systemctl start nginx" + + com := cmdr.NewCmd("systemctl"). + WithArgs([]string{"stop", "nginx"}) + out := com.SafeOutput() + fmt.Println(out) + + com = cmdr.NewCmd("certbot"). + WithArgs([]string{"certonly", "--standalone", "--preferred-challenges", "http", "-d", domain}) + out = com.SafeOutput() + fmt.Println(out) + + com = cmdr.NewCmd("systemctl"). + WithArgs([]string{"start", "nginx"}) + out = com.SafeOutput() + fmt.Println(out) + + return nil + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + golog.Fatal(err) + } +} diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..baf93c7 --- /dev/null +++ b/config.yml @@ -0,0 +1,11 @@ +nginx: + sitesAvailable: /home/pk/golang/hoster/fake/sites-available + sitesEnabled: /home/pk/golang/hoster/fake/sites-enabled + portRangeStart: 10000 + portRangeEnd: 30000 +projects: + root: /home/pk/golang/hoster/fake + owner: 1000 + group: 1000 +templates: + nginx: host.tpl \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8e2d0a3 --- /dev/null +++ b/go.mod @@ -0,0 +1,31 @@ +module hoster + +go 1.23.0 + +require ( + github.com/gookit/config/v2 v2.2.5 + github.com/gookit/goutil v0.6.16 + github.com/kataras/golog v0.1.12 + github.com/urfave/cli/v2 v2.27.4 + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/fatih/color v1.14.1 // indirect + github.com/goccy/go-yaml v1.11.2 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/kataras/pio v0.0.13 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aee000e --- /dev/null +++ b/go.sum @@ -0,0 +1,69 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.11.2 h1:joq77SxuyIs9zzxEjgyLBugMQ9NEgTWxXfz2wVqwAaQ= +github.com/goccy/go-yaml v1.11.2/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/config/v2 v2.2.5 h1:RECbYYbtherywmzn3LNeu9NA5ZqhD7MSKEMsJ7l+MpU= +github.com/gookit/config/v2 v2.2.5/go.mod h1:NeX+yiNYn6Ei10eJvCQFXuHEPIE/IPS8bqaFIsszzaM= +github.com/gookit/goutil v0.6.16 h1:9fRMCF4X9abdRD5+2HhBS/GwafjBlTUBjRtA5dgkvuw= +github.com/gookit/goutil v0.6.16/go.mod h1:op2q8AoPDFSiY2+qkHxcBWQMYxOLQ1GbLXqe7vrwscI= +github.com/gookit/ini/v2 v2.2.3 h1:nSbN+x9OfQPcMObTFP+XuHt8ev6ndv/fWWqxFhPMu2E= +github.com/gookit/ini/v2 v2.2.3/go.mod h1:Vu6p7P7xcfmb8KYu3L0ek8bqu/Im63N81q208SCCZY4= +github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= +github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= +github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= +github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/host.tpl b/host.tpl new file mode 100644 index 0000000..ef01614 --- /dev/null +++ b/host.tpl @@ -0,0 +1,22 @@ +server { + listen 443 ssl http2; listen [::]:443 ssl http2; + server_name {{.Domain}}; + + ssl on; + ssl_certificate /etc/letsencrypt/live/{{.Domain}}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{.Domain}}/privkey.pem; + + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + http2_idle_timeout 5m; # up from 3m default + client_max_body_size 20000M; + + location / { + proxy_pass http://localhost:{{.InternalPort}}/; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + } +} \ No newline at end of file diff --git a/internals/conf/loader.go b/internals/conf/loader.go new file mode 100644 index 0000000..9b58253 --- /dev/null +++ b/internals/conf/loader.go @@ -0,0 +1,24 @@ +package conf + +import ( + "github.com/gookit/config/v2" + "github.com/gookit/config/v2/yaml" + "github.com/kataras/golog" +) + +var cfgPath string +var App *config.Config + +func LoadConfig(path string) *config.Config { + App = config.New("appCfg") + cfgPath = path + App.AddDriver(yaml.Driver) + + err := App.LoadFiles(path) + + if err != nil { + golog.Fatalf("Error loading config: %v", err) + } + + return App +} diff --git a/internals/helpers/findHostConfigs.go b/internals/helpers/findHostConfigs.go new file mode 100644 index 0000000..a49e082 --- /dev/null +++ b/internals/helpers/findHostConfigs.go @@ -0,0 +1,69 @@ +package helpers + +import ( + "bufio" + "hoster/internals/types" + "io/fs" + "os" + "regexp" + "strconv" + "strings" + + "github.com/gookit/goutil/fsutil" +) + +func FindHostConfigs(path string) ([]types.Project, error) { + projects := []types.Project{} + + fsutil.FindInDir(path, func(filePath string, de fs.DirEntry) error { + project := types.Project{} + parseHostFile(filePath, &project) + + if project.Domain != "" { + projects = append(projects, project) + } + return nil + }) + + return projects, nil +} + +var portRegex = regexp.MustCompile(`listen\s+(\d+)`) +var serverNameRegex = regexp.MustCompile(`server_name\s+(.+);`) + +func parseHostFile(filePath string, project *types.Project) error { + file, err := os.Open(filePath) + if err != nil { + return err + } + + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "#") { + continue + } + + if strings.HasPrefix(line, "listen ") { + port := portRegex.FindStringSubmatch(line) + project.InternalPort, err = strconv.Atoi(port[1]) + if err != nil { + return err + } + } + + if strings.HasPrefix(line, "server_name ") { + serverName := serverNameRegex.FindStringSubmatch(line) + project.Domain = serverName[1] + } + } + + if err := scanner.Err(); err != nil { + return err + } + + return nil +} diff --git a/internals/helpers/root.go b/internals/helpers/root.go new file mode 100644 index 0000000..4f583ee --- /dev/null +++ b/internals/helpers/root.go @@ -0,0 +1,7 @@ +package helpers + +import "os" + +func IsRootUser() bool { + return os.Geteuid() == 0 +} diff --git a/internals/templates/templates.go b/internals/templates/templates.go new file mode 100644 index 0000000..baa13e8 --- /dev/null +++ b/internals/templates/templates.go @@ -0,0 +1,40 @@ +package templates + +import ( + _ "embed" + "fmt" + "hoster/internals/conf" + "os" + "path/filepath" + "text/template" + + "github.com/gookit/goutil/fsutil" +) + +func Execute(name string, outPath string, context any) error { + tplPath := filepath.Join(conf.App.MustString(fmt.Sprintf("templates.%s", name))) + + if !fsutil.IsFile(tplPath) { + return fmt.Errorf("template file \"%s\" doesn't exist", tplPath) + } + + outFile, err := os.Create(outPath) + if err != nil { + return err + } + + defer outFile.Close() + + tpl, err := os.ReadFile(tplPath) + if err != nil { + return err + } + + err = template.Must(template.New("tpl").Parse(string(tpl))).Execute(outFile, context) + + if err != nil { + return err + } + + return nil +} diff --git a/internals/types/project.go b/internals/types/project.go new file mode 100644 index 0000000..0171683 --- /dev/null +++ b/internals/types/project.go @@ -0,0 +1,8 @@ +package types + +type Project struct { + Name string + Domain string + HostConfigFile string + InternalPort int +}