First version

This commit is contained in:
trading_peter 2024-08-28 12:31:17 +02:00
commit 407a8d5052
10 changed files with 421 additions and 0 deletions

140
cmd/hoster_cli/hoster.go Normal file
View File

@ -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)
}
}

11
config.yml Normal file
View File

@ -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

31
go.mod Normal file
View File

@ -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
)

69
go.sum Normal file
View File

@ -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=

22
host.tpl Normal file
View File

@ -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;
}
}

24
internals/conf/loader.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package helpers
import "os"
func IsRootUser() bool {
return os.Geteuid() == 0
}

View File

@ -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
}

View File

@ -0,0 +1,8 @@
package types
type Project struct {
Name string
Domain string
HostConfigFile string
InternalPort int
}