Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
caddy
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
caddy
Commits
159b68aa
Commit
159b68aa
authored
Sep 05, 2015
by
Matt Holt
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #228 from tgulacsi/htpasswd
basicauth: Add htpasswd support
parents
d76cf6d3
392f1d70
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
178 additions
and
23 deletions
+178
-23
config/setup/basicauth.go
config/setup/basicauth.go
+18
-2
config/setup/basicauth_test.go
config/setup/basicauth_test.go
+48
-16
middleware/basicauth/basicauth.go
middleware/basicauth/basicauth.go
+79
-2
middleware/basicauth/basicauth_test.go
middleware/basicauth/basicauth_test.go
+33
-3
No files found.
config/setup/basicauth.go
View file @
159b68aa
package
setup
import
(
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
)
...
...
@@ -23,6 +25,7 @@ func BasicAuth(c *Controller) (middleware.Middleware, error) {
func
basicAuthParse
(
c
*
Controller
)
([]
basicauth
.
Rule
,
error
)
{
var
rules
[]
basicauth
.
Rule
var
err
error
for
c
.
Next
()
{
var
rule
basicauth
.
Rule
...
...
@@ -31,7 +34,10 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
switch
len
(
args
)
{
case
2
:
rule
.
Username
=
args
[
0
]
rule
.
Password
=
args
[
1
]
if
rule
.
Password
,
err
=
passwordMatcher
(
rule
.
Username
,
args
[
1
]);
err
!=
nil
{
return
rules
,
c
.
Errf
(
"Get password matcher from %s: %v"
,
c
.
Val
(),
err
)
}
for
c
.
NextBlock
()
{
rule
.
Resources
=
append
(
rule
.
Resources
,
c
.
Val
())
if
c
.
NextArg
()
{
...
...
@@ -41,7 +47,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
case
3
:
rule
.
Resources
=
append
(
rule
.
Resources
,
args
[
0
])
rule
.
Username
=
args
[
1
]
rule
.
Password
=
args
[
2
]
if
rule
.
Password
,
err
=
passwordMatcher
(
rule
.
Username
,
args
[
2
]);
err
!=
nil
{
return
rules
,
c
.
Errf
(
"Get password matcher from %s: %v"
,
c
.
Val
(),
err
)
}
default
:
return
rules
,
c
.
ArgErr
()
}
...
...
@@ -51,3 +59,11 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
return
rules
,
nil
}
func
passwordMatcher
(
username
,
passw
string
)
(
basicauth
.
PasswordMatcher
,
error
)
{
if
!
strings
.
HasPrefix
(
passw
,
"htpasswd="
)
{
return
basicauth
.
PlainMatcher
(
passw
),
nil
}
return
basicauth
.
GetHtpasswdMatcher
(
passw
[
9
:
],
username
)
}
config/setup/basicauth_test.go
View file @
159b68aa
...
...
@@ -2,6 +2,9 @@ package setup
import
(
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/mholt/caddy/middleware/basicauth"
...
...
@@ -30,35 +33,57 @@ func TestBasicAuth(t *testing.T) {
}
func
TestBasicAuthParse
(
t
*
testing
.
T
)
{
htpasswdPasswd
:=
"IedFOuGmTpT8"
htpasswdFile
:=
`sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
var
skipHtpassword
bool
htfh
,
err
:=
ioutil
.
TempFile
(
""
,
"basicauth-"
)
if
err
!=
nil
{
t
.
Logf
(
"Error creating temp file (%v), will skip htpassword test"
)
skipHtpassword
=
true
}
else
{
if
_
,
err
=
htfh
.
Write
([]
byte
(
htpasswdFile
));
err
!=
nil
{
t
.
Fatalf
(
"write htpasswd file %q: %v"
,
htfh
.
Name
(),
err
)
}
htfh
.
Close
()
defer
os
.
Remove
(
htfh
.
Name
())
}
tests
:=
[]
struct
{
input
string
shouldErr
bool
password
string
expected
[]
basicauth
.
Rule
}{
{
`basicauth user pwd`
,
false
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
,
Password
:
"pwd"
},
{
`basicauth user pwd`
,
false
,
"pwd"
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
},
}},
{
`basicauth user pwd {
}`
,
false
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
,
Password
:
"pwd"
},
}`
,
false
,
"pwd"
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
},
}},
{
`basicauth user pwd {
/resource1
/resource2
}`
,
false
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
,
Password
:
"pwd"
,
Resources
:
[]
string
{
"/resource1"
,
"/resource2"
}},
}`
,
false
,
"pwd"
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
,
Resources
:
[]
string
{
"/resource1"
,
"/resource2"
}},
}},
{
`basicauth /resource user pwd`
,
false
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
,
Password
:
"pwd"
,
Resources
:
[]
string
{
"/resource"
}},
{
`basicauth /resource user pwd`
,
false
,
"pwd"
,
[]
basicauth
.
Rule
{
{
Username
:
"user"
,
Resources
:
[]
string
{
"/resource"
}},
}},
{
`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`
,
false
,
[]
basicauth
.
Rule
{
{
Username
:
"user1"
,
Password
:
"pwd1"
,
Resources
:
[]
string
{
"/res1"
}},
{
Username
:
"user2"
,
Password
:
"pwd2"
,
Resources
:
[]
string
{
"/res2"
}},
basicauth /res2 user2 pwd2`
,
false
,
"pwd"
,
[]
basicauth
.
Rule
{
{
Username
:
"user1"
,
Resources
:
[]
string
{
"/res1"
}},
{
Username
:
"user2"
,
Resources
:
[]
string
{
"/res2"
}},
}},
{
`basicauth user`
,
true
,
""
,
[]
basicauth
.
Rule
{}},
{
`basicauth`
,
true
,
""
,
[]
basicauth
.
Rule
{}},
{
`basicauth /resource user pwd asdf`
,
true
,
""
,
[]
basicauth
.
Rule
{}},
{
`basicauth sha1 htpasswd=`
+
htfh
.
Name
(),
false
,
htpasswdPasswd
,
[]
basicauth
.
Rule
{
{
Username
:
"sha1"
},
}},
{
`basicauth user`
,
true
,
[]
basicauth
.
Rule
{}},
{
`basicauth`
,
true
,
[]
basicauth
.
Rule
{}},
{
`basicauth /resource user pwd asdf`
,
true
,
[]
basicauth
.
Rule
{}},
}
for
i
,
test
:=
range
tests
{
...
...
@@ -84,9 +109,16 @@ func TestBasicAuthParse(t *testing.T) {
i
,
j
,
expectedRule
.
Username
,
actualRule
.
Username
)
}
if
actualRule
.
Password
!=
expectedRule
.
Password
{
if
strings
.
Contains
(
test
.
input
,
"htpasswd="
)
&&
skipHtpassword
{
continue
}
pwd
:=
test
.
password
if
len
(
actual
)
>
1
{
pwd
=
fmt
.
Sprintf
(
"%s%d"
,
pwd
,
j
+
1
)
}
if
!
actualRule
.
Password
(
pwd
)
||
actualRule
.
Password
(
test
.
password
+
"!"
)
{
t
.
Errorf
(
"Test %d, rule %d: Expected password '%s', got '%s'"
,
i
,
j
,
expectedRule
.
P
assword
,
actualRule
.
Password
)
i
,
j
,
test
.
p
assword
,
actualRule
.
Password
)
}
expectedRes
:=
fmt
.
Sprintf
(
"%v"
,
expectedRule
.
Resources
)
...
...
middleware/basicauth/basicauth.go
View file @
159b68aa
...
...
@@ -2,9 +2,17 @@
package
basicauth
import
(
"bufio"
"crypto/subtle"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/jimstudt/http-authentication/basic"
"github.com/mholt/caddy/middleware"
)
...
...
@@ -37,7 +45,8 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// Check credentials
if
!
ok
||
username
!=
rule
.
Username
||
subtle
.
ConstantTimeCompare
([]
byte
(
password
),
[]
byte
(
rule
.
Password
))
!=
1
{
!
rule
.
Password
(
password
)
{
//subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 {
continue
}
...
...
@@ -64,6 +73,74 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// file or directory paths.
type
Rule
struct
{
Username
string
Password
string
Password
func
(
string
)
bool
Resources
[]
string
}
type
PasswordMatcher
func
(
pw
string
)
bool
var
(
htpasswords
map
[
string
]
map
[
string
]
PasswordMatcher
htpasswordsMu
sync
.
Mutex
)
func
GetHtpasswdMatcher
(
filename
,
username
string
)
(
PasswordMatcher
,
error
)
{
filename
,
err
:=
filepath
.
Abs
(
filename
)
if
err
!=
nil
{
return
nil
,
err
}
htpasswordsMu
.
Lock
()
if
htpasswords
==
nil
{
htpasswords
=
make
(
map
[
string
]
map
[
string
]
PasswordMatcher
)
}
pm
:=
htpasswords
[
filename
]
if
pm
==
nil
{
fh
,
err
:=
os
.
Open
(
filename
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"open %q: %v"
,
filename
,
err
)
}
defer
fh
.
Close
()
pm
=
make
(
map
[
string
]
PasswordMatcher
)
if
err
=
parseHtpasswd
(
pm
,
fh
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"parsing htpasswd %q: %v"
,
fh
.
Name
(),
err
)
}
htpasswords
[
filename
]
=
pm
}
htpasswordsMu
.
Unlock
()
if
pm
[
username
]
==
nil
{
return
nil
,
fmt
.
Errorf
(
"username %q not found in %q"
,
username
,
filename
)
}
return
pm
[
username
],
nil
}
func
parseHtpasswd
(
pm
map
[
string
]
PasswordMatcher
,
r
io
.
Reader
)
error
{
scanner
:=
bufio
.
NewScanner
(
r
)
for
scanner
.
Scan
()
{
line
:=
strings
.
TrimSpace
(
scanner
.
Text
())
if
line
==
""
||
strings
.
IndexByte
(
line
,
'#'
)
==
0
{
continue
}
i
:=
strings
.
IndexByte
(
line
,
':'
)
if
i
<=
0
{
return
fmt
.
Errorf
(
"malformed line, no color: %q"
,
line
)
}
user
,
encoded
:=
line
[
:
i
],
line
[
i
+
1
:
]
for
_
,
p
:=
range
basic
.
DefaultSystems
{
matcher
,
err
:=
p
(
encoded
)
if
err
!=
nil
{
return
err
}
if
matcher
!=
nil
{
pm
[
user
]
=
matcher
.
MatchesPassword
break
}
}
}
return
scanner
.
Err
()
}
func
PlainMatcher
(
passw
string
)
PasswordMatcher
{
return
func
(
pw
string
)
bool
{
return
subtle
.
ConstantTimeCompare
([]
byte
(
pw
),
[]
byte
(
passw
))
==
1
}
}
middleware/basicauth/basicauth_test.go
View file @
159b68aa
...
...
@@ -3,8 +3,10 @@ package basicauth
import
(
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/mholt/caddy/middleware"
...
...
@@ -15,7 +17,7 @@ func TestBasicAuth(t *testing.T) {
rw
:=
BasicAuth
{
Next
:
middleware
.
HandlerFunc
(
contentHandler
),
Rules
:
[]
Rule
{
{
Username
:
"test"
,
Password
:
"ttest"
,
Resources
:
[]
string
{
"/testing"
}},
{
Username
:
"test"
,
Password
:
PlainMatcher
(
"ttest"
)
,
Resources
:
[]
string
{
"/testing"
}},
},
}
...
...
@@ -66,8 +68,8 @@ func TestMultipleOverlappingRules(t *testing.T) {
rw
:=
BasicAuth
{
Next
:
middleware
.
HandlerFunc
(
contentHandler
),
Rules
:
[]
Rule
{
{
Username
:
"t"
,
Password
:
"p1"
,
Resources
:
[]
string
{
"/t"
}},
{
Username
:
"t1"
,
Password
:
"p2"
,
Resources
:
[]
string
{
"/t/t"
}},
{
Username
:
"t"
,
Password
:
PlainMatcher
(
"p1"
)
,
Resources
:
[]
string
{
"/t"
}},
{
Username
:
"t1"
,
Password
:
PlainMatcher
(
"p2"
)
,
Resources
:
[]
string
{
"/t/t"
}},
},
}
...
...
@@ -111,3 +113,31 @@ func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
fmt
.
Fprintf
(
w
,
r
.
URL
.
String
())
return
http
.
StatusOK
,
nil
}
func
TestHtpasswd
(
t
*
testing
.
T
)
{
htpasswdPasswd
:=
"IedFOuGmTpT8"
htpasswdFile
:=
`sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
htfh
,
err
:=
ioutil
.
TempFile
(
""
,
"basicauth-"
)
if
err
!=
nil
{
t
.
Skipf
(
"Error creating temp file (%v), will skip htpassword test"
)
return
}
if
_
,
err
=
htfh
.
Write
([]
byte
(
htpasswdFile
));
err
!=
nil
{
t
.
Fatalf
(
"write htpasswd file %q: %v"
,
htfh
.
Name
(),
err
)
}
htfh
.
Close
()
defer
os
.
Remove
(
htfh
.
Name
())
for
i
,
username
:=
range
[]
string
{
"sha1"
,
"md5"
}
{
rule
:=
Rule
{
Username
:
username
,
Resources
:
[]
string
{
"/testing"
}}
if
rule
.
Password
,
err
=
GetHtpasswdMatcher
(
htfh
.
Name
(),
rule
.
Username
);
err
!=
nil
{
t
.
Fatalf
(
"GetHtpasswdMatcher(%q, %q): %v"
,
htfh
.
Name
(),
rule
.
Username
,
err
)
}
t
.
Logf
(
"%d. username=%q password=%v"
,
i
,
rule
.
Username
,
rule
.
Password
)
if
!
rule
.
Password
(
htpasswdPasswd
)
||
rule
.
Password
(
htpasswdPasswd
+
"!"
)
{
t
.
Errorf
(
"%d (%s) password does not match."
,
i
,
rule
.
Username
)
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment