diff --git a/README.md b/README.md index 0beb8c4..9494010 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 @@ -28,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 @@ -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/) 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) +} 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_check.go b/internal/command/tool_check.go new file mode 100644 index 0000000..55ebe9f --- /dev/null +++ b/internal/command/tool_check.go @@ -0,0 +1,240 @@ +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" +} + +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) + } + } + + ch := &check{wg: new(sync.WaitGroup), httpClient: c.HTTPClient} + ch.wg.Add(5) + + 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 ch.checkIP(hostingInfo, domain) + + 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", 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", 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 +} + +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 { + 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 + } + } + } + + c.ipv4 = yesno(ipv4) + c.ipv6 = yesno(ipv6) +} + +func yesno(value bool) string { + if value { + return "yes" + } + + return "no" +} + +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 { + c.validCert = fmt.Sprintf("no, hostname doesn't match: %s", err) + } + + c.validCert = "yes" + return + } + + c.validCert = fmt.Sprintf("no, %s", err) +} + +func (c *check) checkEnforceHTTPS(domain string) { + defer c.wg.Done() + + url := fmt.Sprintf("http://%s", domain) + + noRedirectClient := c.httpClient + noRedirectClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return nil } + + res, err := noRedirectClient.Get(url) + if err != nil { + 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)) { + c.enforceHTTPS = "yes" + return + } + + c.enforceHTTPS = "no" +} + +func (c *check) checkProtocol(domain string) { + defer c.wg.Done() + + res, err := http.Get("https://" + domain) + if err != nil { + c.protocol = err.Error() + return + } + res.Body.Close() + + c.protocol = res.Proto +} + +func (c *check) checCustom404(domain string) { + defer c.wg.Done() + + res, err := c.httpClient.Get(fmt.Sprintf("https://%s/thispagedoesnotexists", domain)) + if err != nil { + c.custom404 = err.Error() + return + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + 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.

") { + c.custom404 = "no" + return + } + + c.custom404 = "yes" +} 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 { 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 }, 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