From 717d6b46c80752ec2005036bb0dabbd934ce5a1d Mon Sep 17 00:00:00 2001 From: Maxime Le Conte des Floris Date: Sat, 10 Dec 2022 17:20:16 +0100 Subject: [PATCH 1/6] update README --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 0beb8c4..1eef31f 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ PHP) websites. - It requires at least a Pro plan (for SSH access). - The underlying file system is made invisible: deploying a website with a domain www.example.com will upload the content to a www.example.com folder. This is by design and it can't be overridden. -- A few operations are asynchronous and create a task on OVHcloud infrastructure that will be picked up by robots and executed. Therefore, some operations may take several seconds or more. -- When attaching a domain to a hosting, you'll have to wait ~1h for the Let's Encrypt SSL certificates. # Usage @@ -57,11 +55,6 @@ Force `go test` to run all the tests (by disabling caching) ./scrits/test.sh ``` -## Tools - -- check domains -- ci - ## License [MIT](https://choosealicense.com/licenses/mit/) From 3e1e0d09d7dc46529c18ae0c66421e8ae1195487 Mon Sep 17 00:00:00 2001 From: Maxime Le Conte des Floris Date: Sat, 10 Dec 2022 17:21:23 +0100 Subject: [PATCH 2/6] fix: wording in help --- README.md | 2 +- internal/command/remove.go | 2 +- internal/command/tasks.go | 4 ++-- internal/command/tool_ci.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1eef31f..9494010 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Available commands are: logs View access logs open Open browser to current deployed website remove Remove websites (files & attached domains) - tasks Lists tasks + tasks List tasks tool Group useful extra-commands users Manage users whoami Show info about the user currently logged in diff --git a/internal/command/remove.go b/internal/command/remove.go index 86978fa..cb71d5c 100644 --- a/internal/command/remove.go +++ b/internal/command/remove.go @@ -22,7 +22,7 @@ func (c *RemoveCommand) Help() string { helpText := ` Usage: owh remove, rm [] - Remove websites (files & attached domains). + Removes websites (files & attached domains). Options: --hosting service name (if not set, you'll be prompt) diff --git a/internal/command/tasks.go b/internal/command/tasks.go index 0945dad..b4085ba 100644 --- a/internal/command/tasks.go +++ b/internal/command/tasks.go @@ -14,13 +14,13 @@ func (c *TasksCommand) Help() string { helpText := ` Usage: owh tasks - Lists tasks + Lists tasks. ` return strings.TrimSpace(helpText) } func (c *TasksCommand) Synopsis() string { - return "Lists tasks" + return "List tasks" } func (c *TasksCommand) Run(args []string) int { diff --git a/internal/command/tool_ci.go b/internal/command/tool_ci.go index 4ca72a0..c1a24e3 100644 --- a/internal/command/tool_ci.go +++ b/internal/command/tool_ci.go @@ -13,7 +13,7 @@ func (c *CICommand) Help() string { helpText := ` Usage: owh tool ci - Help you setup a deployment in CI. + Helps you setup a deployment in CI. /!\ This command will display secrets in the terminal. ` @@ -21,7 +21,7 @@ Usage: owh tool ci } func (c *CICommand) Synopsis() string { - return "Shows useful info to setup a CI" + return "Show useful info to setup a CI" } func (c *CICommand) Run(args []string) int { From 77705bd4dfa6ed8149157b5122ee91a77fa345b8 Mon Sep 17 00:00:00 2001 From: Maxime Le Conte des Floris Date: Sat, 10 Dec 2022 20:19:54 +0100 Subject: [PATCH 3/6] feat: add tool check command --- internal/command/tool_check.go | 263 +++++++++++++++++++++++++++++++++ main.go | 3 + 2 files changed, 266 insertions(+) create mode 100644 internal/command/tool_check.go diff --git a/internal/command/tool_check.go b/internal/command/tool_check.go new file mode 100644 index 0000000..b08076a --- /dev/null +++ b/internal/command/tool_check.go @@ -0,0 +1,263 @@ +package command + +import ( + "crypto/tls" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + "sync" + "text/tabwriter" + + "go.mlcdf.fr/owh/internal/api" + "go.mlcdf.fr/owh/internal/cmdutil" + "go.mlcdf.fr/owh/internal/config" +) + +type CheckCommand struct { + App +} + +func (c *CheckCommand) Help() string { + helpText := ` +Usage: owh tool check + + Performs various check on your website such as: + - check DNS config + - validate SSL certs + - test http => https redirection + - etc +` + return strings.TrimSpace(helpText) +} + +func (c *CheckCommand) Synopsis() string { + return "Perform various check on your website" +} + +type checks struct { + ipv4 string + ipv6 string + + protocol string + validCert string + enforceHTTPS string + custom404 string +} + +type setCheck func(c *checks) + +func (c *CheckCommand) Run(args []string) int { + var hosting string + var domain string + + flags := flag.NewFlagSet("link", flag.ExitOnError) + + flags.StringVar(&domain, "domain", "", "") + + if err := flags.Parse(args); err != nil { + return c.View.PrintErr(err) + } + + if domain == "" { + link, err := c.EnsureLink() + if err != nil && err != config.ErrFolderNotLinked { + return c.View.PrintErr(err) + } + + domain = link.CanonicalDomain + hosting = link.Hosting + } + + client, err := c.LoggedClient() + if err != nil { + return c.View.PrintErr(err) + } + + if hosting == "" { + hosting, err = client.HostingByDomain(domain) + if err != nil { + return c.View.PrintErr(err) + } + } + + fc := make(chan setCheck, 5) + + wg := new(sync.WaitGroup) + wg.Add(5) + + go checkRedirect(fc, wg, domain) + go checkValidCert(fc, wg, domain) + go checkProtocol(fc, wg, domain) + go checkCustom404Page(fc, wg, c.HTTPClient, domain) + + hostingInfo, err := client.GetHosting(hosting) + if err != nil { + return c.View.PrintErr(err) + } + + go checkIP(fc, wg, hostingInfo, domain) + + wg.Wait() + close(fc) + + _checks := &checks{} + for ch := range fc { + ch(_checks) + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + defer w.Flush() + + fmt.Fprintf(w, cmdutil.Bold("DNS")+"\t\t\n") + fmt.Fprintf(w, " Record A \t=\t%s\n", _checks.ipv4) + fmt.Fprintf(w, " Record AAAA \t=\t%s\n", _checks.ipv6) + fmt.Fprintf(w, "\t\t\n") + fmt.Fprintf(w, cmdutil.Bold("HTTP")+"\t\t\n") + + fmt.Fprintf(w, " Protocol\t=\t%s\n", _checks.protocol) + fmt.Fprintf(w, " Valid certificate\t=\t%s\n", _checks.validCert) + fmt.Fprintf(w, " Enforce HTTPS\t=\t%s\n", _checks.enforceHTTPS) + fmt.Fprintf(w, " Custom 404 page\t=\t%s\n", _checks.custom404) + + return 0 +} + +func checkIP(e chan setCheck, wg *sync.WaitGroup, hosting *api.HostingInfo, domain string) { + defer wg.Done() + + ips, err := net.LookupIP(domain) + if err != nil { + e <- func(c *checks) { + c.ipv4 = "failed to lookup IP" + c.ipv6 = "failed to lookup IP" + } + return + } + + var ipv4 bool + var ipv6 bool + + for _, ip := range ips { + _ip := ip.To4() + + if _ip != nil { + if _ip.String() == hosting.HostingIP { + ipv4 = true + } + } else { + if ip.To16().String() == hosting.HostingIPv6 { + ipv6 = true + } + } + } + + e <- func(c *checks) { + c.ipv4 = yesno(ipv4) + c.ipv6 = yesno(ipv6) + } +} + +func yesno(value bool) string { + if value { + return "yes" + } + + return "no" +} + +func checkValidCert(e chan setCheck, wg *sync.WaitGroup, domain string) { + defer wg.Done() + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:443", domain), nil) + if err == nil { + err = conn.VerifyHostname(domain) + if err != nil { + e <- func(c *checks) { + c.validCert = fmt.Sprintf("no, hostname doesn't match: %s", err) + } + return + } + + e <- func(c *checks) { + c.validCert = "yes" + } + return + } + + e <- func(c *checks) { + c.validCert = fmt.Sprintf("no, %s", err) + } +} + +func checkRedirect(e chan setCheck, wg *sync.WaitGroup, domain string) { + defer wg.Done() + + url := fmt.Sprintf("http://%s", domain) + + noRedirectClient := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + res, err := noRedirectClient.Get(url) + if err != nil { + e <- func(c *checks) { c.enforceHTTPS = err.Error() } + return + } + + res.Body.Close() + + if (res.StatusCode == 301 || res.StatusCode == 302) && + strings.Contains(res.Header.Get("location"), fmt.Sprintf("https://%s", domain)) { + e <- func(c *checks) { c.enforceHTTPS = "yes" } + return + } + + e <- func(c *checks) { c.enforceHTTPS = "no" } +} + +func checkProtocol(e chan setCheck, wg *sync.WaitGroup, domain string) { + defer wg.Done() + + res, err := http.Get("https://" + domain) + if err != nil { + e <- func(c *checks) { c.protocol = err.Error() } + return + } + res.Body.Close() + + e <- func(c *checks) { + c.protocol = res.Proto + } +} + +func checkCustom404Page(e chan setCheck, wg *sync.WaitGroup, httpClient *http.Client, domain string) { + defer wg.Done() + + res, err := httpClient.Get(fmt.Sprintf("https://%s/thispagedoesnotexists", domain)) + if err != nil { + e <- func(c *checks) { c.custom404 = err.Error() } + return + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + e <- func(c *checks) { c.custom404 = err.Error() } + return + } + + if strings.Contains(string(body), "404 Not Found") && + strings.Contains(string(body), "

The requested URL was not found on this server.

") { + e <- func(c *checks) { c.custom404 = "no" } + return + } + + e <- func(c *checks) { c.custom404 = "yes" } +} diff --git a/main.go b/main.go index fdd7ba3..7f484c7 100644 --- a/main.go +++ b/main.go @@ -135,6 +135,9 @@ func main() { "tool": func() (cli.Command, error) { return &command.ToolCommand{App: *app}, nil }, + "tool check": func() (cli.Command, error) { + return &command.CheckCommand{App: *app}, nil + }, "tool ci": func() (cli.Command, error) { return &command.CICommand{App: *app}, nil }, From 4855200b11590568ed29a51172d3301fb6ad6355 Mon Sep 17 00:00:00 2001 From: Maxime Le Conte des Floris Date: Sat, 10 Dec 2022 20:32:52 +0100 Subject: [PATCH 4/6] refacto: tool check --- internal/command/tool_check.go | 135 ++++++++++++++------------------- 1 file changed, 56 insertions(+), 79 deletions(-) diff --git a/internal/command/tool_check.go b/internal/command/tool_check.go index b08076a..55ebe9f 100644 --- a/internal/command/tool_check.go +++ b/internal/command/tool_check.go @@ -38,18 +38,6 @@ func (c *CheckCommand) Synopsis() string { return "Perform various check on your website" } -type checks struct { - ipv4 string - ipv6 string - - protocol string - validCert string - enforceHTTPS string - custom404 string -} - -type setCheck func(c *checks) - func (c *CheckCommand) Run(args []string) int { var hosting string var domain string @@ -84,57 +72,60 @@ func (c *CheckCommand) Run(args []string) int { } } - fc := make(chan setCheck, 5) + ch := &check{wg: new(sync.WaitGroup), httpClient: c.HTTPClient} + ch.wg.Add(5) - wg := new(sync.WaitGroup) - wg.Add(5) - - go checkRedirect(fc, wg, domain) - go checkValidCert(fc, wg, domain) - go checkProtocol(fc, wg, domain) - go checkCustom404Page(fc, wg, c.HTTPClient, domain) + go ch.checkEnforceHTTPS(domain) + go ch.checkValidCert(domain) + go ch.checkProtocol(domain) + go ch.checCustom404(domain) hostingInfo, err := client.GetHosting(hosting) if err != nil { return c.View.PrintErr(err) } - go checkIP(fc, wg, hostingInfo, domain) + go ch.checkIP(hostingInfo, domain) - wg.Wait() - close(fc) - - _checks := &checks{} - for ch := range fc { - ch(_checks) - } + ch.wg.Wait() w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) defer w.Flush() fmt.Fprintf(w, cmdutil.Bold("DNS")+"\t\t\n") - fmt.Fprintf(w, " Record A \t=\t%s\n", _checks.ipv4) - fmt.Fprintf(w, " Record AAAA \t=\t%s\n", _checks.ipv6) + fmt.Fprintf(w, " Record A \t=\t%s\n", ch.ipv4) + fmt.Fprintf(w, " Record AAAA \t=\t%s\n", ch.ipv6) fmt.Fprintf(w, "\t\t\n") fmt.Fprintf(w, cmdutil.Bold("HTTP")+"\t\t\n") - fmt.Fprintf(w, " Protocol\t=\t%s\n", _checks.protocol) - fmt.Fprintf(w, " Valid certificate\t=\t%s\n", _checks.validCert) - fmt.Fprintf(w, " Enforce HTTPS\t=\t%s\n", _checks.enforceHTTPS) - fmt.Fprintf(w, " Custom 404 page\t=\t%s\n", _checks.custom404) + fmt.Fprintf(w, " Protocol\t=\t%s\n", ch.protocol) + fmt.Fprintf(w, " Valid certificate\t=\t%s\n", ch.validCert) + fmt.Fprintf(w, " Enforce HTTPS\t=\t%s\n", ch.enforceHTTPS) + fmt.Fprintf(w, " Custom 404 page\t=\t%s\n", ch.custom404) return 0 } -func checkIP(e chan setCheck, wg *sync.WaitGroup, hosting *api.HostingInfo, domain string) { - defer wg.Done() +type check struct { + wg *sync.WaitGroup + httpClient *http.Client + + ipv4 string + ipv6 string + + protocol string + validCert string + enforceHTTPS string + custom404 string +} + +func (c *check) checkIP(hosting *api.HostingInfo, domain string) { + defer c.wg.Done() ips, err := net.LookupIP(domain) if err != nil { - e <- func(c *checks) { - c.ipv4 = "failed to lookup IP" - c.ipv6 = "failed to lookup IP" - } + c.ipv4 = "failed to lookup IP" + c.ipv6 = "failed to lookup IP" return } @@ -155,10 +146,8 @@ func checkIP(e chan setCheck, wg *sync.WaitGroup, hosting *api.HostingInfo, doma } } - e <- func(c *checks) { - c.ipv4 = yesno(ipv4) - c.ipv6 = yesno(ipv6) - } + c.ipv4 = yesno(ipv4) + c.ipv6 = yesno(ipv6) } func yesno(value bool) string { @@ -169,44 +158,34 @@ func yesno(value bool) string { return "no" } -func checkValidCert(e chan setCheck, wg *sync.WaitGroup, domain string) { - defer wg.Done() +func (c *check) checkValidCert(domain string) { + defer c.wg.Done() conn, err := tls.Dial("tcp", fmt.Sprintf("%s:443", domain), nil) if err == nil { err = conn.VerifyHostname(domain) if err != nil { - e <- func(c *checks) { - c.validCert = fmt.Sprintf("no, hostname doesn't match: %s", err) - } - return + c.validCert = fmt.Sprintf("no, hostname doesn't match: %s", err) } - e <- func(c *checks) { - c.validCert = "yes" - } + c.validCert = "yes" return } - e <- func(c *checks) { - c.validCert = fmt.Sprintf("no, %s", err) - } + c.validCert = fmt.Sprintf("no, %s", err) } -func checkRedirect(e chan setCheck, wg *sync.WaitGroup, domain string) { - defer wg.Done() +func (c *check) checkEnforceHTTPS(domain string) { + defer c.wg.Done() url := fmt.Sprintf("http://%s", domain) - noRedirectClient := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } + noRedirectClient := c.httpClient + noRedirectClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return nil } res, err := noRedirectClient.Get(url) if err != nil { - e <- func(c *checks) { c.enforceHTTPS = err.Error() } + c.enforceHTTPS = err.Error() return } @@ -214,34 +193,32 @@ func checkRedirect(e chan setCheck, wg *sync.WaitGroup, domain string) { if (res.StatusCode == 301 || res.StatusCode == 302) && strings.Contains(res.Header.Get("location"), fmt.Sprintf("https://%s", domain)) { - e <- func(c *checks) { c.enforceHTTPS = "yes" } + c.enforceHTTPS = "yes" return } - e <- func(c *checks) { c.enforceHTTPS = "no" } + c.enforceHTTPS = "no" } -func checkProtocol(e chan setCheck, wg *sync.WaitGroup, domain string) { - defer wg.Done() +func (c *check) checkProtocol(domain string) { + defer c.wg.Done() res, err := http.Get("https://" + domain) if err != nil { - e <- func(c *checks) { c.protocol = err.Error() } + c.protocol = err.Error() return } res.Body.Close() - e <- func(c *checks) { - c.protocol = res.Proto - } + c.protocol = res.Proto } -func checkCustom404Page(e chan setCheck, wg *sync.WaitGroup, httpClient *http.Client, domain string) { - defer wg.Done() +func (c *check) checCustom404(domain string) { + defer c.wg.Done() - res, err := httpClient.Get(fmt.Sprintf("https://%s/thispagedoesnotexists", domain)) + res, err := c.httpClient.Get(fmt.Sprintf("https://%s/thispagedoesnotexists", domain)) if err != nil { - e <- func(c *checks) { c.custom404 = err.Error() } + c.custom404 = err.Error() return } @@ -249,15 +226,15 @@ func checkCustom404Page(e chan setCheck, wg *sync.WaitGroup, httpClient *http.Cl body, err := io.ReadAll(res.Body) if err != nil { - e <- func(c *checks) { c.custom404 = err.Error() } + c.custom404 = err.Error() return } if strings.Contains(string(body), "404 Not Found") && strings.Contains(string(body), "

The requested URL was not found on this server.

") { - e <- func(c *checks) { c.custom404 = "no" } + c.custom404 = "no" return } - e <- func(c *checks) { c.custom404 = "yes" } + c.custom404 = "yes" } From ae85134803feaaf347fa1a5ea0bd6c1887b2e48a Mon Sep 17 00:00:00 2001 From: Maxime Le Conte des Floris Date: Sat, 10 Dec 2022 20:34:08 +0100 Subject: [PATCH 5/6] remove venom tests & test data --- tests/test-project/index.html | 12 ------------ tests/venom/0_init.yml | 13 ------------- tests/venom/auth.yml | 19 ------------------- tests/venom/deploy.yml | 10 ---------- tests/venom/list.yml | 17 ----------------- 5 files changed, 71 deletions(-) delete mode 100644 tests/test-project/index.html delete mode 100644 tests/venom/0_init.yml delete mode 100644 tests/venom/auth.yml delete mode 100644 tests/venom/deploy.yml delete mode 100644 tests/venom/list.yml diff --git a/tests/test-project/index.html b/tests/test-project/index.html deleted file mode 100644 index 17441da..0000000 --- a/tests/test-project/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - Hello world - - - Hello world! - - \ No newline at end of file diff --git a/tests/venom/0_init.yml b/tests/venom/0_init.yml deleted file mode 100644 index 6045017..0000000 --- a/tests/venom/0_init.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: initialize venom tests -testcases: -- name: build owh - steps: - - type: exec - script: | - cd ../.. - go build -o {{.owh}} . - - assertions: - - result.code ShouldEqual 0 - - result.systemout ShouldEqual "" - - result.systemerr ShouldEqual "" \ No newline at end of file diff --git a/tests/venom/auth.yml b/tests/venom/auth.yml deleted file mode 100644 index 83247e1..0000000 --- a/tests/venom/auth.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: auth -testcases: -- name: unauthenticated deploy - steps: - - script: "{{.owh}} deploy" - type: exec - assertions: - - result.code ShouldEqual 1 - - "result.systemout ShouldContainSubstring Please run: owh login first" - - result.systemerr ShouldEqual "" - -- name: unauthenticated deploy in CI - steps: - - script: "CI=1 {{.owh}} deploy" - type: exec - assertions: - - result.code ShouldEqual 1 - - "result.systemout ShouldContainSubstring To use owh in automation, set the OWH_CONSUMER_KEY environment variable." - - result.systemerr ShouldEqual "" diff --git a/tests/venom/deploy.yml b/tests/venom/deploy.yml deleted file mode 100644 index dabe495..0000000 --- a/tests/venom/deploy.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: deploy -testcases: -- name: deploy - steps: - - script: OWH_HOSTING="lhudfzo.cluster031.hosting.ovh.net" OWH_CANONICAL_DOMAIN="test.mlcdf.fr" {{.owh}} deploy --dir tests/test-project - type: exec - assertions: - - result.code ShouldEqual 1 - - "result.systemout ShouldContainSubstring Please set a hosting via the --hosting flag or by running: owh hosting myhosting" - - result.systemerr ShouldEqual "" diff --git a/tests/venom/list.yml b/tests/venom/list.yml deleted file mode 100644 index 520c115..0000000 --- a/tests/venom/list.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: domain:list -testcases: -- name: list without hosting - steps: - - script: "{{.owh}} domains:list" - type: exec - assertions: - - result.code ShouldEqual 1 - - "result.systemout ShouldContainSubstring Please set a hosting via the --hosting flag or by running: owh hosting myhosting" - - result.systemerr ShouldEqual "" - -- name: domain:list with hosting - steps: - - script: "{{.owh}} domains:list --hosting lhudfzo.cluster031.hosting.ovh.net" - type: exec - assertions: - - result.code ShouldEqual 0 From fbed1445b1a9659c14562fecd3db20e6e7a007e2 Mon Sep 17 00:00:00 2001 From: Maxime Le Conte des Floris Date: Sat, 10 Dec 2022 20:41:20 +0100 Subject: [PATCH 6/6] fix: add missing func --- internal/cmdutil/cmdutil.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/cmdutil/cmdutil.go b/internal/cmdutil/cmdutil.go index 9f77e73..b808ca5 100644 --- a/internal/cmdutil/cmdutil.go +++ b/internal/cmdutil/cmdutil.go @@ -30,3 +30,7 @@ func Highlight(str string) string { func Special(str string) string { return lipgloss.NewStyle().Foreground(StyleSpecial).Render(str) } + +func Bold(str string) string { + return lipgloss.NewStyle().Bold(true).Render(str) +}