@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 302 lines 9.47 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddyauth
16
17 import (
18 "bytes"
19 "encoding/json"
20 "errors"
21 "io"
22 "os"
23 "strings"
24 "testing"
25
26 "github.com/spf13/pflag"
27
28 "github.com/caddyserver/caddy/v2"
29 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
30 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
31 caddycmd "github.com/caddyserver/caddy/v2/cmd"
32 )
33
34 func init() {
35 caddy.RegisterModule(testHash{})
36 caddy.RegisterModule(testCompareOnlyHash{})
37 caddy.RegisterModule(testMCFHash{})
38 }
39
40 // testHash is a third-party-style hash module used to
41 // test that modules in the http.authentication.hashes
42 // namespace can be used by name.
43 type testHash struct {
44 provisioned bool
45 }
46
47 func (testHash) CaddyModule() caddy.ModuleInfo {
48 return caddy.ModuleInfo{
49 ID: "http.authentication.hashes.testhash",
50 New: func() caddy.Module { return new(testHash) },
51 }
52 }
53
54 func (th *testHash) Provision(caddy.Context) error {
55 th.provisioned = true
56 return nil
57 }
58
59 func (testHash) Compare(hashed, plaintext []byte) (bool, error) {
60 return bytes.Equal(hashed, append([]byte("testhash:"), plaintext...)), nil
61 }
62
63 func (th testHash) Hash(plaintext []byte) ([]byte, error) {
64 if !th.provisioned {
65 return nil, errors.New("testhash: not provisioned")
66 }
67 return append([]byte("testhash:"), plaintext...), nil
68 }
69
70 func (testHash) FakeHash() []byte { return []byte("testhash:fake") }
71
72 // testCompareOnlyHash can compare, but not generate, hashes.
73 type testCompareOnlyHash struct{}
74
75 func (testCompareOnlyHash) CaddyModule() caddy.ModuleInfo {
76 return caddy.ModuleInfo{
77 ID: "http.authentication.hashes.testcompareonly",
78 New: func() caddy.Module { return new(testCompareOnlyHash) },
79 }
80 }
81
82 func (testCompareOnlyHash) Compare(hashed, plaintext []byte) (bool, error) {
83 return bytes.Equal(hashed, plaintext), nil
84 }
85
86 // testMCFHash produces hashes that start with "$", like Modular Crypt Format.
87 type testMCFHash struct{}
88
89 func (testMCFHash) CaddyModule() caddy.ModuleInfo {
90 return caddy.ModuleInfo{
91 ID: "http.authentication.hashes.testmcf",
92 New: func() caddy.Module { return new(testMCFHash) },
93 }
94 }
95
96 func (testMCFHash) Compare(hashed, plaintext []byte) (bool, error) {
97 return bytes.Equal(hashed, append([]byte("$testmcf$"), plaintext...)), nil
98 }
99
100 func (testMCFHash) Hash(plaintext []byte) ([]byte, error) {
101 return append([]byte("$testmcf$"), plaintext...), nil
102 }
103
104 func (testMCFHash) FakeHash() []byte { return []byte("$testmcf$fake") }
105
106 func adaptBasicAuth(t *testing.T, input string) (string, error) {
107 t.Helper()
108 adapter := caddyfile.Adapter{ServerType: httpcaddyfile.ServerType{}}
109 result, _, err := adapter.Adapt([]byte(input), map[string]any{"filename": "Caddyfile"})
110 if err != nil {
111 return "", err
112 }
113
114 var cfg struct {
115 Apps struct {
116 HTTP struct {
117 Servers map[string]struct {
118 Routes []struct {
119 Handle []struct {
120 Providers struct {
121 HTTPBasic struct {
122 Hash struct {
123 Algorithm string `json:"algorithm"`
124 } `json:"hash"`
125 } `json:"http_basic"`
126 } `json:"providers"`
127 } `json:"handle"`
128 } `json:"routes"`
129 } `json:"servers"`
130 } `json:"http"`
131 } `json:"apps"`
132 }
133 if err := json.Unmarshal(result, &cfg); err != nil {
134 t.Fatalf("unmarshaling adapted config: %v\n%s", err, result)
135 }
136 srv, ok := cfg.Apps.HTTP.Servers["srv0"]
137 if !ok || len(srv.Routes) != 1 || len(srv.Routes[0].Handle) != 1 {
138 t.Fatalf("unexpected adapted config: %s", result)
139 }
140 return srv.Routes[0].Handle[0].Providers.HTTPBasic.Hash.Algorithm, nil
141 }
142
143 func TestBasicAuthCaddyfileHashModules(t *testing.T) {
144 for _, tc := range []struct {
145 name string
146 algorithm string
147 expected string
148 }{
149 {name: "default", algorithm: "", expected: "bcrypt"},
150 {name: "bcrypt", algorithm: "bcrypt", expected: "bcrypt"},
151 {name: "argon2id", algorithm: "argon2id", expected: "argon2id"},
152 {name: "third-party", algorithm: "testhash", expected: "testhash"},
153 {name: "third-party compare only", algorithm: "testcompareonly", expected: "testcompareonly"},
154 } {
155 t.Run(tc.name, func(t *testing.T) {
156 input := ":80 {\n\tbasic_auth " + tc.algorithm + " {\n\t\talice dGVzdGhhc2g6c2VjcmV0\n\t}\n}\n"
157 algorithm, err := adaptBasicAuth(t, input)
158 if err != nil {
159 t.Fatalf("unexpected error: %v", err)
160 }
161 if algorithm != tc.expected {
162 t.Errorf("expected hash algorithm %q, got %q", tc.expected, algorithm)
163 }
164 })
165 }
166 }
167
168 func TestBasicAuthCaddyfileUnknownHash(t *testing.T) {
169 _, err := adaptBasicAuth(t, ":80 {\n\tbasic_auth nope {\n\t\talice c2VjcmV0\n\t}\n}\n")
170 if err == nil {
171 t.Fatal("expected an error for an unknown hash algorithm, got nil")
172 }
173 for _, want := range []string{"unrecognized hash algorithm: nope", "argon2id", "bcrypt", "testhash"} {
174 if !strings.Contains(err.Error(), want) {
175 t.Errorf("expected error to contain %q, got: %v", want, err)
176 }
177 }
178 }
179
180 func TestBasicAuthProvisionHashModule(t *testing.T) {
181 ctx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
182 defer cancel()
183
184 hba := HTTPBasicAuth{
185 HashRaw: json.RawMessage(`{"algorithm": "testhash"}`),
186 AccountList: []Account{{
187 Username: "alice",
188 Password: "dGVzdGhhc2g6c2VjcmV0", // base64("testhash:secret")
189 }},
190 HashCache: new(Cache),
191 }
192 if err := hba.Provision(ctx); err != nil {
193 t.Fatalf("provisioning: %v", err)
194 }
195 if _, ok := hba.Hash.(*testHash); !ok {
196 t.Fatalf("expected hash module to be *testHash, got %T", hba.Hash)
197 }
198 if string(hba.fakePassword) != "testhash:fake" {
199 t.Errorf("expected fake password from module, got %q", hba.fakePassword)
200 }
201 if ok, err := hba.correctPassword(hba.Accounts["alice"], []byte("secret")); err != nil || !ok {
202 t.Errorf("expected password to match (err=%v)", err)
203 }
204 if ok, err := hba.correctPassword(hba.Accounts["alice"], []byte("wrong")); err != nil || ok {
205 t.Errorf("expected password not to match (err=%v)", err)
206 }
207 }
208
209 func runHashPassword(t *testing.T, algorithm, plaintext string) (string, error) {
210 t.Helper()
211
212 fs := pflag.NewFlagSet("hash-password", pflag.ContinueOnError)
213 fs.String("plaintext", plaintext, "")
214 fs.String("algorithm", algorithm, "")
215 fs.Int("bcrypt-cost", defaultBcryptCost, "")
216 fs.Uint32("argon2id-time", defaultArgon2idTime, "")
217 fs.Uint32("argon2id-memory", defaultArgon2idMemory, "")
218 fs.Uint8("argon2id-threads", defaultArgon2idThreads, "")
219 fs.Uint32("argon2id-keylen", defaultArgon2idKeylen, "")
220
221 r, w, err := os.Pipe()
222 if err != nil {
223 t.Fatal(err)
224 }
225 stdout := os.Stdout
226 os.Stdout = w
227 _, cmdErr := cmdHashPassword(caddycmd.Flags{FlagSet: fs})
228 os.Stdout = stdout
229 _ = w.Close()
230
231 out, err := io.ReadAll(r)
232 if err != nil {
233 t.Fatal(err)
234 }
235 return strings.TrimSpace(string(out)), cmdErr
236 }
237
238 func TestHashPasswordCommandHashModule(t *testing.T) {
239 out, err := runHashPassword(t, "testhash", "secret")
240 if err != nil {
241 t.Fatalf("unexpected error: %v", err)
242 }
243 if out != "dGVzdGhhc2g6c2VjcmV0" { // base64("testhash:secret")
244 t.Errorf("expected output %q, got %q", "dGVzdGhhc2g6c2VjcmV0", out)
245 }
246
247 out, err = runHashPassword(t, "testmcf", "secret")
248 if err != nil {
249 t.Fatalf("unexpected error: %v", err)
250 }
251 // A leading "$" does not make a third-party hash MCF, so it is
252 // base64-encoded like any other module output.
253 if out != "JHRlc3RtY2Ykc2VjcmV0" { // base64("$testmcf$secret")
254 t.Errorf("expected output %q, got %q", "JHRlc3RtY2Ykc2VjcmV0", out)
255 }
256 }
257
258 // TestHashPasswordCommandRoundTrip checks that the output of the
259 // hash-password command is accepted by HTTPBasicAuth as an account password.
260 func TestHashPasswordCommandRoundTrip(t *testing.T) {
261 for _, algorithm := range []string{"testhash", "testmcf", "bcrypt", "argon2id"} {
262 t.Run(algorithm, func(t *testing.T) {
263 out, err := runHashPassword(t, algorithm, "secret")
264 if err != nil {
265 t.Fatalf("hash-password: %v", err)
266 }
267
268 ctx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
269 defer cancel()
270 hba := HTTPBasicAuth{
271 HashRaw: json.RawMessage(`{"algorithm": "` + algorithm + `"}`),
272 AccountList: []Account{{Username: "alice", Password: out}},
273 }
274 if err := hba.Provision(ctx); err != nil {
275 t.Fatalf("provisioning with %q: %v", out, err)
276 }
277 if ok, err := hba.correctPassword(hba.Accounts["alice"], []byte("secret")); err != nil || !ok {
278 t.Errorf("expected password to match (err=%v)", err)
279 }
280 if ok, err := hba.correctPassword(hba.Accounts["alice"], []byte("wrong")); err != nil || ok {
281 t.Errorf("expected wrong password not to match (err=%v)", err)
282 }
283 })
284 }
285 }
286
287 func TestHashPasswordCommandErrors(t *testing.T) {
288 _, err := runHashPassword(t, "nope", "secret")
289 if err == nil {
290 t.Fatal("expected an error for an unknown hash algorithm, got nil")
291 }
292 for _, want := range []string{"unrecognized hash algorithm: nope", "argon2id", "bcrypt", "testhash"} {
293 if !strings.Contains(err.Error(), want) {
294 t.Errorf("expected error to contain %q, got: %v", want, err)
295 }
296 }
297
298 _, err = runHashPassword(t, "testcompareonly", "secret")
299 if err == nil || !strings.Contains(err.Error(), "does not support generating hashes") {
300 t.Errorf("expected an error for a hash module that cannot hash, got: %v", err)
301 }
302 }