forked from TrueCloudLab/lego
Merge pull request #5 from xenolf/simplehttp-checks
Finish SimpleHTTP and add tests
This commit is contained in:
commit
0c862a7d53
4 changed files with 167 additions and 9 deletions
|
@ -35,7 +35,7 @@ type User interface {
|
|||
|
||||
// Interface for all challenge solvers to implement.
|
||||
type solver interface {
|
||||
CanSolve() bool
|
||||
CanSolve(domain string) bool
|
||||
Solve(challenge challenge, domain string) error
|
||||
}
|
||||
|
||||
|
@ -150,7 +150,7 @@ func (c *Client) solveChallenges(challenges []*authorizationResource) error {
|
|||
// loop through the resources, basically through the domains.
|
||||
for _, authz := range challenges {
|
||||
// no solvers - no solving
|
||||
if solvers := c.chooseSolvers(authz.Body); solvers != nil {
|
||||
if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
|
||||
for i, solver := range solvers {
|
||||
// TODO: do not immediately fail if one domain fails to validate.
|
||||
err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
|
||||
|
@ -168,11 +168,11 @@ func (c *Client) solveChallenges(challenges []*authorizationResource) error {
|
|||
|
||||
// Checks all combinations from the server and returns an array of
|
||||
// solvers which should get executed in series.
|
||||
func (c *Client) chooseSolvers(auth authorization) map[int]solver {
|
||||
func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver {
|
||||
for _, combination := range auth.Combinations {
|
||||
solvers := make(map[int]solver)
|
||||
for _, idx := range combination {
|
||||
if solver, ok := c.Solvers[auth.Challenges[idx].Type]; ok {
|
||||
if solver, ok := c.Solvers[auth.Challenges[idx].Type]; ok && solver.CanSolve(domain) {
|
||||
solvers[idx] = solver
|
||||
} else {
|
||||
logger().Printf("Could not find solver for: %s", auth.Challenges[idx].Type)
|
||||
|
|
|
@ -2,7 +2,7 @@ package acme
|
|||
|
||||
type dvsniChallenge struct{}
|
||||
|
||||
func (s *dvsniChallenge) CanSolve() bool {
|
||||
func (s *dvsniChallenge) CanSolve(domain string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,11 @@ import (
|
|||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -21,9 +23,40 @@ type simpleHTTPChallenge struct {
|
|||
optPort string
|
||||
}
|
||||
|
||||
func (s *simpleHTTPChallenge) CanSolve() bool {
|
||||
// SimpleHTTPS checks for DNS, public IP and port bindings
|
||||
func (s *simpleHTTPChallenge) CanSolve(domain string) bool {
|
||||
// determine public ip
|
||||
resp, err := http.Get("https://icanhazip.com/")
|
||||
if err != nil {
|
||||
logger().Printf("Could not get public IP -> %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ip, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger().Printf("Could not get public IP -> %v", err)
|
||||
return false
|
||||
}
|
||||
ipStr := string(ip)
|
||||
ipStr = strings.Replace(ipStr, "\n", "", -1)
|
||||
|
||||
// resolve domain we should solve for
|
||||
resolvedIPs, err := net.LookupHost(domain)
|
||||
if err != nil {
|
||||
logger().Printf("Could not lookup DNS A record for %s", domain)
|
||||
return false
|
||||
}
|
||||
|
||||
// if the resolve does not resolve to our public ip, we can't solve.
|
||||
for _, resolvedIP := range resolvedIPs {
|
||||
if resolvedIP == ipStr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
logger().Printf("SimpleHTTPS: Domain %s does not resolve to the public ip of this server. Determined ip: %s", domain, ipStr)
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *simpleHTTPChallenge) Solve(chlng challenge, domain string) error {
|
||||
|
||||
|
@ -64,8 +97,10 @@ loop:
|
|||
case "pending":
|
||||
break
|
||||
case "invalid":
|
||||
listener.Close()
|
||||
return errors.New("The server could not validate our request.")
|
||||
default:
|
||||
listener.Close()
|
||||
return errors.New("The server returned an unexpected state.")
|
||||
}
|
||||
|
||||
|
@ -94,7 +129,6 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp
|
|||
tempCertPEM,
|
||||
pemBytes)
|
||||
if err != nil {
|
||||
logger().Print("error here!")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -111,7 +145,7 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp
|
|||
|
||||
tlsListener, err := tls.Listen("tcp", port, tlsConf)
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not start HTTP listener! -> %v", err)
|
||||
return nil, fmt.Errorf("Could not start HTTP listener! -> %v", err)
|
||||
}
|
||||
|
||||
// The handler validates the HOST header and request type.
|
||||
|
|
124
acme/simple_http_challenge_test.go
Normal file
124
acme/simple_http_challenge_test.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/square/go-jose"
|
||||
)
|
||||
|
||||
func TestSimpleHTTPCanSolve(t *testing.T) {
|
||||
challenge := &simpleHTTPChallenge{}
|
||||
|
||||
// determine public ip
|
||||
resp, err := http.Get("https://icanhazip.com/")
|
||||
if err != nil {
|
||||
t.Errorf("Could not get public IP -> %v", err)
|
||||
}
|
||||
|
||||
ip, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Could not get public IP -> %v", err)
|
||||
}
|
||||
ipStr := string(ip)
|
||||
|
||||
if expected, actual := false, challenge.CanSolve("google.com"); expected != actual {
|
||||
t.Errorf("Expected CanSolve to return %t for domain 'google.com' but was %t", expected, actual)
|
||||
}
|
||||
|
||||
localResolv := strings.Replace(ipStr, "\n", "", -1) + ".xip.io"
|
||||
if expected, actual := true, challenge.CanSolve(localResolv); expected != actual {
|
||||
t.Errorf("Expected CanSolve to return %t for domain 'localhost' but was %t", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleHTTP(t *testing.T) {
|
||||
privKey, err := generatePrivateKey(512)
|
||||
if err != nil {
|
||||
t.Errorf("Could not generate public key -> %v", err)
|
||||
}
|
||||
jws := &jws{privKey: privKey}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
}))
|
||||
|
||||
solver := &simpleHTTPChallenge{jws: jws}
|
||||
clientChallenge := challenge{Type: "simpleHttps", Status: "pending", URI: ts.URL, Token: "123456789"}
|
||||
|
||||
// validate error on non-root bind to 443
|
||||
if err = solver.Solve(clientChallenge, "test.domain"); err == nil {
|
||||
t.Error("BIND: Expected Solve to return an error but the error was nil.")
|
||||
}
|
||||
|
||||
// Validate error on unexpected state
|
||||
solver.optPort = "8080"
|
||||
if err = solver.Solve(clientChallenge, "test.domain"); err == nil {
|
||||
t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.")
|
||||
}
|
||||
|
||||
// Validate error on invalid status
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
failed := challenge{Type: "simpleHttps", Status: "invalid", URI: ts.URL, Token: "123456789"}
|
||||
jsonBytes, _ := json.Marshal(&failed)
|
||||
w.Write(jsonBytes)
|
||||
})
|
||||
if err = solver.Solve(clientChallenge, "test.domain"); err == nil {
|
||||
t.Error("FAILED: Expected Solve to return an error but the error was nil.")
|
||||
}
|
||||
|
||||
// Validate no error on valid response
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
valid := challenge{Type: "simpleHttps", Status: "valid", URI: ts.URL, Token: "123456789"}
|
||||
jsonBytes, _ := json.Marshal(&valid)
|
||||
w.Write(jsonBytes)
|
||||
})
|
||||
if err = solver.Solve(clientChallenge, "test.domain"); err != nil {
|
||||
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
|
||||
}
|
||||
|
||||
// Validate server on port 8080 which responds appropriately
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var request challenge
|
||||
|
||||
clientJws, _ := ioutil.ReadAll(r.Body)
|
||||
j, err := jose.ParseSigned(string(clientJws))
|
||||
if err != nil {
|
||||
t.Errorf("Client sent invalid JWS to the server. -> %v", err)
|
||||
}
|
||||
output, err := j.Verify(&privKey.PublicKey)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to verify client data -> %v", err)
|
||||
}
|
||||
json.Unmarshal(output, &request)
|
||||
|
||||
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
reqURL := "https://localhost:8080/.well-known/acme-challenge/" + request.Path
|
||||
t.Logf("Request URL is: %s", reqURL)
|
||||
req, _ := http.NewRequest("GET", reqURL, nil)
|
||||
req.Host = "test.domain"
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("Expected the solver to listen on port 8080 -> %v", err)
|
||||
}
|
||||
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
bodyStr := string(body)
|
||||
if bodyStr != "123456789" {
|
||||
t.Errorf("Expected the solver to return the token %s but instead returned '%s'", "123456789", bodyStr)
|
||||
}
|
||||
|
||||
valid := challenge{Type: "simpleHttps", Status: "valid", URI: ts.URL, Token: "123456789"}
|
||||
jsonBytes, _ := json.Marshal(&valid)
|
||||
w.Write(jsonBytes)
|
||||
})
|
||||
if err = solver.Solve(clientChallenge, "test.domain"); err != nil {
|
||||
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue