1package resolver
2
3import (
4	"crypto/rand"
5	"crypto/rsa"
6	"fmt"
7	"io"
8	"net/http"
9	"sort"
10	"testing"
11
12	"github.com/go-acme/lego/v4/acme"
13	"github.com/go-acme/lego/v4/acme/api"
14	"github.com/go-acme/lego/v4/platform/tester"
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/require"
17	"gopkg.in/square/go-jose.v2"
18)
19
20func TestByType(t *testing.T) {
21	challenges := []acme.Challenge{
22		{Type: "dns-01"}, {Type: "tlsalpn-01"}, {Type: "http-01"},
23	}
24
25	sort.Sort(byType(challenges))
26
27	expected := []acme.Challenge{
28		{Type: "tlsalpn-01"}, {Type: "http-01"}, {Type: "dns-01"},
29	}
30
31	assert.Equal(t, expected, challenges)
32}
33
34func TestValidate(t *testing.T) {
35	mux, apiURL, tearDown := tester.SetupFakeAPI()
36	defer tearDown()
37
38	var statuses []string
39
40	privateKey, _ := rsa.GenerateKey(rand.Reader, 512)
41
42	mux.HandleFunc("/chlg", func(w http.ResponseWriter, r *http.Request) {
43		if r.Method != http.MethodPost {
44			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
45			return
46		}
47
48		if err := validateNoBody(privateKey, r); err != nil {
49			http.Error(w, err.Error(), http.StatusBadRequest)
50			return
51		}
52
53		w.Header().Set("Link", "<"+apiURL+`/my-authz>; rel="up"`)
54
55		st := statuses[0]
56		statuses = statuses[1:]
57
58		chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}
59		if st == acme.StatusInvalid {
60			chlg.Error = &acme.ProblemDetails{}
61		}
62
63		err := tester.WriteJSONResponse(w, chlg)
64		if err != nil {
65			http.Error(w, err.Error(), http.StatusInternalServerError)
66			return
67		}
68	})
69
70	mux.HandleFunc("/my-authz", func(w http.ResponseWriter, r *http.Request) {
71		if r.Method != http.MethodPost {
72			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
73			return
74		}
75
76		st := statuses[0]
77		statuses = statuses[1:]
78
79		authorization := acme.Authorization{
80			Status:     st,
81			Challenges: []acme.Challenge{},
82		}
83
84		if st == acme.StatusInvalid {
85			chlg := acme.Challenge{
86				Status: acme.StatusInvalid,
87				Error:  &acme.ProblemDetails{},
88			}
89			authorization.Challenges = append(authorization.Challenges, chlg)
90		}
91
92		err := tester.WriteJSONResponse(w, authorization)
93		if err != nil {
94			http.Error(w, err.Error(), http.StatusInternalServerError)
95			return
96		}
97	})
98
99	core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
100	require.NoError(t, err)
101
102	testCases := []struct {
103		name     string
104		statuses []string
105		want     string
106	}{
107		{
108			name:     "POST-unexpected",
109			statuses: []string{"weird"},
110			want:     "unexpected",
111		},
112		{
113			name:     "POST-valid",
114			statuses: []string{acme.StatusValid},
115		},
116		{
117			name:     "POST-invalid",
118			statuses: []string{acme.StatusInvalid},
119			want:     "error",
120		},
121		{
122			name:     "POST-pending-unexpected",
123			statuses: []string{acme.StatusPending, "weird"},
124			want:     "unexpected",
125		},
126		{
127			name:     "POST-pending-valid",
128			statuses: []string{acme.StatusPending, acme.StatusValid},
129		},
130		{
131			name:     "POST-pending-invalid",
132			statuses: []string{acme.StatusPending, acme.StatusInvalid},
133			want:     "error",
134		},
135	}
136
137	for _, test := range testCases {
138		t.Run(test.name, func(t *testing.T) {
139			statuses = test.statuses
140
141			err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: apiURL + "/chlg"})
142			if test.want == "" {
143				require.NoError(t, err)
144			} else {
145				require.Error(t, err)
146				assert.Contains(t, err.Error(), test.want)
147			}
148		})
149	}
150}
151
152// validateNoBody reads the http.Request POST body, parses the JWS and validates it to read the body.
153// If there is an error doing this,
154// or if the JWS body is not the empty JSON payload "{}" or a POST-as-GET payload "" an error is returned.
155// We use this to verify challenge POSTs to the ts below do not send a JWS body.
156func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error {
157	reqBody, err := io.ReadAll(r.Body)
158	if err != nil {
159		return err
160	}
161
162	jws, err := jose.ParseSigned(string(reqBody))
163	if err != nil {
164		return err
165	}
166
167	body, err := jws.Verify(&jose.JSONWebKey{
168		Key:       privateKey.Public(),
169		Algorithm: "RSA",
170	})
171	if err != nil {
172		return err
173	}
174
175	if bodyStr := string(body); bodyStr != "{}" && bodyStr != "" {
176		return fmt.Errorf(`expected JWS POST body "{}" or "", got %q`, bodyStr)
177	}
178	return nil
179}
180