Merge pull request #5 from xenolf/simplehttp-checks

Finish SimpleHTTP and add tests
This commit is contained in:
xenolf 2015-06-13 21:17:49 +02:00
commit 0c862a7d53
4 changed files with 167 additions and 9 deletions

View file

@ -35,7 +35,7 @@ type User interface {
// Interface for all challenge solvers to implement. // Interface for all challenge solvers to implement.
type solver interface { type solver interface {
CanSolve() bool CanSolve(domain string) bool
Solve(challenge challenge, domain string) error 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. // loop through the resources, basically through the domains.
for _, authz := range challenges { for _, authz := range challenges {
// no solvers - no solving // 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 { for i, solver := range solvers {
// TODO: do not immediately fail if one domain fails to validate. // TODO: do not immediately fail if one domain fails to validate.
err := solver.Solve(authz.Body.Challenges[i], authz.Domain) 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 // Checks all combinations from the server and returns an array of
// solvers which should get executed in series. // 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 { for _, combination := range auth.Combinations {
solvers := make(map[int]solver) solvers := make(map[int]solver)
for _, idx := range combination { 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 solvers[idx] = solver
} else { } else {
logger().Printf("Could not find solver for: %s", auth.Challenges[idx].Type) logger().Printf("Could not find solver for: %s", auth.Challenges[idx].Type)

View file

@ -2,7 +2,7 @@ package acme
type dvsniChallenge struct{} type dvsniChallenge struct{}
func (s *dvsniChallenge) CanSolve() bool { func (s *dvsniChallenge) CanSolve(domain string) bool {
return false return false
} }

View file

@ -10,9 +10,11 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"math/big" "math/big"
"net" "net"
"net/http" "net/http"
"strings"
"time" "time"
) )
@ -21,9 +23,40 @@ type simpleHTTPChallenge struct {
optPort string 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 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 { func (s *simpleHTTPChallenge) Solve(chlng challenge, domain string) error {
@ -64,8 +97,10 @@ loop:
case "pending": case "pending":
break break
case "invalid": case "invalid":
listener.Close()
return errors.New("The server could not validate our request.") return errors.New("The server could not validate our request.")
default: default:
listener.Close()
return errors.New("The server returned an unexpected state.") return errors.New("The server returned an unexpected state.")
} }
@ -94,7 +129,6 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp
tempCertPEM, tempCertPEM,
pemBytes) pemBytes)
if err != nil { if err != nil {
logger().Print("error here!")
return nil, err return nil, err
} }
@ -111,7 +145,7 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp
tlsListener, err := tls.Listen("tcp", port, tlsConf) tlsListener, err := tls.Listen("tcp", port, tlsConf)
if err != nil { 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. // The handler validates the HOST header and request type.

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