├── LICENSE.md
├── README.md
├── authorization.go
├── authorization_test.go
├── digest_auth_client.go
├── tests
├── .htdigest
├── docker-compose.yml
├── httpd.conf
└── manual-test.go
└── www_authenticate.go
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Xinyun Zhou All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 |
9 | 2. Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-http-digest-auth-client
2 |
3 |
4 | Golang Http Digest Authentication Client
5 |
6 | This client implements [RFC7616 HTTP Digest Access Authentication](https://www.rfc-editor.org/rfc/rfc7616.txt)
7 | and by now the basic features should work.
8 |
9 | # Usage
10 |
11 | ```go
12 | // import
13 | import dac "github.com/xinsnake/go-http-digest-auth-client"
14 |
15 | // create a new digest authentication request
16 | dr := dac.NewRequest(username, password, method, uri, payload)
17 | response1, err := dr.Execute()
18 |
19 | // check error, get response
20 |
21 | // reuse the existing digest authentication request so no extra request is needed
22 | dr.UpdateRequest(username, password, method, uri, payload)
23 | response2, err := dr.Execute()
24 |
25 | // check error, get response
26 | ```
27 |
28 | Or you can use it with `http.Request`
29 |
30 | ```go
31 | t := dac.NewTransport(username, password)
32 | req, err := http.NewRequest(method, uri, payload)
33 |
34 | if err != nil {
35 | log.Fatalln(err)
36 | }
37 |
38 | resp, err := t.RoundTrip(req)
39 | if err != nil {
40 | log.Fatalln(err)
41 | }
42 | defer resp.Body.Close()
43 |
44 | fmt.Println(resp)
45 | ```
46 |
--------------------------------------------------------------------------------
/authorization.go:
--------------------------------------------------------------------------------
1 | package digest_auth_client
2 |
3 | import (
4 | "bytes"
5 | "crypto/md5"
6 | "crypto/sha256"
7 | "encoding/hex"
8 | "fmt"
9 | "hash"
10 | "io"
11 | "net/url"
12 | "strings"
13 | "time"
14 | )
15 |
16 | type authorization struct {
17 | Algorithm string // unquoted
18 | Cnonce string // quoted
19 | Nc int // unquoted
20 | Nonce string // quoted
21 | Opaque string // quoted
22 | Qop string // unquoted
23 | Realm string // quoted
24 | Response string // quoted
25 | URI string // quoted
26 | Userhash bool // quoted
27 | Username string // quoted
28 | Username_ string // quoted
29 | }
30 |
31 | func newAuthorization(dr *DigestRequest) (*authorization, error) {
32 |
33 | ah := authorization{
34 | Algorithm: dr.Wa.Algorithm,
35 | Cnonce: "",
36 | Nc: 0,
37 | Nonce: dr.Wa.Nonce,
38 | Opaque: dr.Wa.Opaque,
39 | Qop: "",
40 | Realm: dr.Wa.Realm,
41 | Response: "",
42 | URI: "",
43 | Userhash: dr.Wa.Userhash,
44 | Username: "",
45 | Username_: "", // TODO
46 | }
47 |
48 | return ah.refreshAuthorization(dr)
49 | }
50 |
51 | const (
52 | algorithmMD5 = "MD5"
53 | algorithmMD5Sess = "MD5-SESS"
54 | algorithmSHA256 = "SHA-256"
55 | algorithmSHA256Sess = "SHA-256-SESS"
56 | )
57 |
58 | func (ah *authorization) refreshAuthorization(dr *DigestRequest) (*authorization, error) {
59 |
60 | ah.Username = dr.Username
61 |
62 | if ah.Userhash {
63 | ah.Username = ah.hash(fmt.Sprintf("%s:%s", ah.Username, ah.Realm))
64 | }
65 |
66 | ah.Nc++
67 |
68 | ah.Cnonce = ah.hash(fmt.Sprintf("%d:%s:my_value", time.Now().UnixNano(), dr.Username))
69 |
70 | url, err := url.Parse(dr.URI)
71 | if err != nil {
72 | return nil, err
73 | }
74 |
75 | ah.URI = url.RequestURI()
76 | ah.Response = ah.computeResponse(dr)
77 |
78 | return ah, nil
79 | }
80 |
81 | func (ah *authorization) computeResponse(dr *DigestRequest) (s string) {
82 |
83 | kdSecret := ah.hash(ah.computeA1(dr))
84 | kdData := fmt.Sprintf("%s:%08x:%s:%s:%s", ah.Nonce, ah.Nc, ah.Cnonce, ah.Qop, ah.hash(ah.computeA2(dr)))
85 |
86 | return ah.hash(fmt.Sprintf("%s:%s", kdSecret, kdData))
87 | }
88 |
89 | func (ah *authorization) computeA1(dr *DigestRequest) string {
90 |
91 | algorithm := strings.ToUpper(ah.Algorithm)
92 |
93 | if algorithm == "" || algorithm == algorithmMD5 || algorithm == algorithmSHA256 {
94 | return fmt.Sprintf("%s:%s:%s", ah.Username, ah.Realm, dr.Password)
95 | }
96 |
97 | if algorithm == algorithmMD5Sess || algorithm == algorithmSHA256Sess {
98 | upHash := ah.hash(fmt.Sprintf("%s:%s:%s", ah.Username, ah.Realm, dr.Password))
99 | return fmt.Sprintf("%s:%s:%s", upHash, ah.Nonce, ah.Cnonce)
100 | }
101 |
102 | return ""
103 | }
104 |
105 | func (ah *authorization) computeA2(dr *DigestRequest) string {
106 |
107 | if strings.Contains(dr.Wa.Qop, "auth-int") {
108 | ah.Qop = "auth-int"
109 | return fmt.Sprintf("%s:%s:%s", dr.Method, ah.URI, ah.hash(dr.Body))
110 | }
111 |
112 | if dr.Wa.Qop == "auth" || dr.Wa.Qop == "" {
113 | ah.Qop = "auth"
114 | return fmt.Sprintf("%s:%s", dr.Method, ah.URI)
115 | }
116 |
117 | return ""
118 | }
119 |
120 | func (ah *authorization) hash(a string) string {
121 | var h hash.Hash
122 | algorithm := strings.ToUpper(ah.Algorithm)
123 |
124 | if algorithm == "" || algorithm == algorithmMD5 || algorithm == algorithmMD5Sess {
125 | h = md5.New()
126 | } else if algorithm == algorithmSHA256 || algorithm == algorithmSHA256Sess {
127 | h = sha256.New()
128 | } else {
129 | // unknown algorithm
130 | return ""
131 | }
132 |
133 | io.WriteString(h, a)
134 | return hex.EncodeToString(h.Sum(nil))
135 | }
136 |
137 | func (ah *authorization) toString() string {
138 | var buffer bytes.Buffer
139 |
140 | buffer.WriteString("Digest ")
141 |
142 | if ah.Username != "" {
143 | buffer.WriteString(fmt.Sprintf("username=\"%s\", ", ah.Username))
144 | }
145 |
146 | if ah.Realm != "" {
147 | buffer.WriteString(fmt.Sprintf("realm=\"%s\", ", ah.Realm))
148 | }
149 |
150 | if ah.Nonce != "" {
151 | buffer.WriteString(fmt.Sprintf("nonce=\"%s\", ", ah.Nonce))
152 | }
153 |
154 | if ah.URI != "" {
155 | buffer.WriteString(fmt.Sprintf("uri=\"%s\", ", ah.URI))
156 | }
157 |
158 | if ah.Response != "" {
159 | buffer.WriteString(fmt.Sprintf("response=\"%s\", ", ah.Response))
160 | }
161 |
162 | if ah.Algorithm != "" {
163 | buffer.WriteString(fmt.Sprintf("algorithm=%s, ", ah.Algorithm))
164 | }
165 |
166 | if ah.Cnonce != "" {
167 | buffer.WriteString(fmt.Sprintf("cnonce=\"%s\", ", ah.Cnonce))
168 | }
169 |
170 | if ah.Opaque != "" {
171 | buffer.WriteString(fmt.Sprintf("opaque=\"%s\", ", ah.Opaque))
172 | }
173 |
174 | if ah.Qop != "" {
175 | buffer.WriteString(fmt.Sprintf("qop=%s, ", ah.Qop))
176 | }
177 |
178 | if ah.Nc != 0 {
179 | buffer.WriteString(fmt.Sprintf("nc=%08x, ", ah.Nc))
180 | }
181 |
182 | if ah.Userhash {
183 | buffer.WriteString("userhash=true, ")
184 | }
185 |
186 | s := buffer.String()
187 |
188 | return strings.TrimSuffix(s, ", ")
189 | }
190 |
--------------------------------------------------------------------------------
/authorization_test.go:
--------------------------------------------------------------------------------
1 | package digest_auth_client
2 |
3 | import "testing"
4 |
5 | func TestHash(t *testing.T) {
6 | testCases := []struct {
7 | name string
8 | algorithm string
9 | expRes string
10 | }{
11 | {
12 | name: "empty algorithm",
13 | algorithm: "",
14 | expRes: "1a79a4d60de6718e8e5b326e338ae533",
15 | },
16 | {
17 | name: "MD5 algorithm",
18 | algorithm: "MD5",
19 | expRes: "1a79a4d60de6718e8e5b326e338ae533",
20 | },
21 | {
22 | name: "MD5-sess algorithm",
23 | algorithm: "MD5",
24 | expRes: "1a79a4d60de6718e8e5b326e338ae533",
25 | },
26 | {
27 | name: "SHA256 algorithm",
28 | algorithm: "SHA-256",
29 | expRes: "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
30 | },
31 | {
32 | name: "SHA256-sess algorithm",
33 | algorithm: "SHA-256",
34 | expRes: "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
35 | },
36 | {
37 | name: "md5 algorithm",
38 | algorithm: "md5",
39 | expRes: "1a79a4d60de6718e8e5b326e338ae533",
40 | },
41 | {
42 | name: "unknown algorithm",
43 | algorithm: "unknown",
44 | expRes: "",
45 | },
46 | }
47 |
48 | for _, tc := range testCases {
49 | t.Run(tc.name, func(t *testing.T) {
50 | ah := &authorization{Algorithm: tc.algorithm}
51 | res := ah.hash("example")
52 | if res != tc.expRes {
53 | t.Errorf("got: %q, want: %q", res, tc.expRes)
54 | }
55 | })
56 | }
57 | }
58 |
59 | func TestComputeA1(t *testing.T) {
60 | testCases := []struct {
61 | name string
62 | algorithm string
63 | expRes string
64 | }{
65 | {
66 | name: "empty algorithm",
67 | algorithm: "",
68 | expRes: "username:realm:secret",
69 | },
70 | {
71 | name: "MD5 algorithm",
72 | algorithm: "MD5",
73 | expRes: "username:realm:secret",
74 | },
75 | {
76 | name: "MD5-sess algorithm",
77 | algorithm: "MD5",
78 | expRes: "username:realm:secret",
79 | },
80 | {
81 | name: "SHA256 algorithm",
82 | algorithm: "SHA-256",
83 | expRes: "username:realm:secret",
84 | },
85 | {
86 | name: "SHA256-sess algorithm",
87 | algorithm: "SHA-256",
88 | expRes: "username:realm:secret",
89 | },
90 | {
91 | name: "md5 algorithm",
92 | algorithm: "md5",
93 | expRes: "username:realm:secret",
94 | },
95 | {
96 | name: "unknown algorithm",
97 | algorithm: "unknown",
98 | expRes: "",
99 | },
100 | }
101 |
102 | for _, tc := range testCases {
103 | t.Run(tc.name, func(t *testing.T) {
104 | dr := &DigestRequest{Password: "secret"}
105 | ah := &authorization{
106 | Algorithm: tc.algorithm,
107 | Nonce: "nonce",
108 | Cnonce: "cnonce",
109 | Username: "username",
110 | Realm: "realm",
111 | }
112 | res := ah.computeA1(dr)
113 | if res != tc.expRes {
114 | t.Errorf("got: %q, want: %q", res, tc.expRes)
115 | }
116 | })
117 | }
118 | }
119 |
120 | func TestComputeA2(t *testing.T) {
121 | testCases := []struct {
122 | name string
123 | qop string
124 | expRes string
125 | expAuthQop string
126 | }{
127 | {
128 | name: "empty qop",
129 | qop: "",
130 | expRes: "method:uri",
131 | expAuthQop: "auth",
132 | },
133 | {
134 | name: "qop is auth",
135 | qop: "auth",
136 | expRes: "method:uri",
137 | expAuthQop: "auth",
138 | },
139 | {
140 | name: "qop is auth-int",
141 | qop: "qop is auth-int",
142 | expRes: "method:uri:841a2d689ad86bd1611447453c22c6fc",
143 | expAuthQop: "auth-int",
144 | },
145 | }
146 |
147 | for _, tc := range testCases {
148 | t.Run(tc.name, func(t *testing.T) {
149 | dr := &DigestRequest{
150 | Method: "method",
151 | Body: "body",
152 | Wa: &wwwAuthenticate{
153 | Qop: tc.qop,
154 | },
155 | }
156 | ah := &authorization{
157 | Algorithm: "MD5",
158 | Nonce: "nonce",
159 | Cnonce: "cnonce",
160 | Username: "username",
161 | Realm: "realm",
162 | URI: "uri",
163 | Qop: tc.qop,
164 | }
165 | res := ah.computeA2(dr)
166 | if res != tc.expRes {
167 | t.Errorf("wrong result, got: %q, want: %q", res, tc.expRes)
168 | }
169 | if ah.Qop != tc.expAuthQop {
170 | t.Errorf("wrong qop, got: %q, want: %q", ah.Qop, tc.expAuthQop)
171 | }
172 | })
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/digest_auth_client.go:
--------------------------------------------------------------------------------
1 | package digest_auth_client
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "fmt"
7 | "net/http"
8 | "time"
9 | )
10 |
11 | type DigestRequest struct {
12 | Body string
13 | Method string
14 | Password string
15 | URI string
16 | Username string
17 | Header http.Header
18 | Auth *authorization
19 | Wa *wwwAuthenticate
20 | CertVal bool
21 | HTTPClient *http.Client
22 | }
23 |
24 | type DigestTransport struct {
25 | Password string
26 | Username string
27 | HTTPClient *http.Client
28 | }
29 |
30 | // NewRequest creates a new DigestRequest object
31 | func NewRequest(username, password, method, uri, body string) DigestRequest {
32 | dr := DigestRequest{}
33 | dr.UpdateRequest(username, password, method, uri, body)
34 | dr.CertVal = true
35 | return dr
36 | }
37 |
38 | // NewTransport creates a new DigestTransport object
39 | func NewTransport(username, password string) DigestTransport {
40 | dt := DigestTransport{}
41 | dt.Password = password
42 | dt.Username = username
43 | return dt
44 | }
45 |
46 | func (dr *DigestRequest) getHTTPClient() *http.Client {
47 | if dr.HTTPClient != nil {
48 | return dr.HTTPClient
49 | }
50 | tlsConfig := tls.Config{}
51 | if !dr.CertVal {
52 | tlsConfig.InsecureSkipVerify = true
53 | }
54 |
55 | return &http.Client{
56 | Timeout: 30 * time.Second,
57 | Transport: &http.Transport{
58 | TLSClientConfig: &tlsConfig,
59 | },
60 | }
61 | }
62 |
63 | // UpdateRequest is called when you want to reuse an existing
64 | // DigestRequest connection with new request information
65 | func (dr *DigestRequest) UpdateRequest(username, password, method, uri, body string) *DigestRequest {
66 | dr.Body = body
67 | dr.Method = method
68 | dr.Password = password
69 | dr.URI = uri
70 | dr.Username = username
71 | dr.Header = make(map[string][]string)
72 | return dr
73 | }
74 |
75 | // RoundTrip implements the http.RoundTripper interface
76 | func (dt *DigestTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
77 | username := dt.Username
78 | password := dt.Password
79 | method := req.Method
80 | uri := req.URL.String()
81 |
82 | var body string
83 | if req.Body != nil {
84 | buf := new(bytes.Buffer)
85 | buf.ReadFrom(req.Body)
86 | body = buf.String()
87 | }
88 |
89 | dr := NewRequest(username, password, method, uri, body)
90 | if dt.HTTPClient != nil {
91 | dr.HTTPClient = dt.HTTPClient
92 | }
93 |
94 | return dr.Execute()
95 | }
96 |
97 | // Execute initialise the request and get a response
98 | func (dr *DigestRequest) Execute() (resp *http.Response, err error) {
99 |
100 | if dr.Auth != nil {
101 | return dr.executeExistingDigest()
102 | }
103 |
104 | var req *http.Request
105 | if req, err = http.NewRequest(dr.Method, dr.URI, bytes.NewReader([]byte(dr.Body))); err != nil {
106 | return nil, err
107 | }
108 | req.Header = dr.Header
109 |
110 | client := dr.getHTTPClient()
111 |
112 | if resp, err = client.Do(req); err != nil {
113 | return nil, err
114 | }
115 |
116 | if resp.StatusCode == 401 {
117 | return dr.executeNewDigest(resp)
118 | }
119 |
120 | // return the resp to user to handle resp.body.Close()
121 | return resp, nil
122 | }
123 |
124 | func (dr *DigestRequest) executeNewDigest(resp *http.Response) (resp2 *http.Response, err error) {
125 | var (
126 | auth *authorization
127 | wa *wwwAuthenticate
128 | waString string
129 | )
130 |
131 | // body not required for authentication, closing
132 | resp.Body.Close()
133 |
134 | if waString = resp.Header.Get("WWW-Authenticate"); waString == "" {
135 | return nil, fmt.Errorf("failed to get WWW-Authenticate header, please check your server configuration")
136 | }
137 | wa = newWwwAuthenticate(waString)
138 | dr.Wa = wa
139 |
140 | if auth, err = newAuthorization(dr); err != nil {
141 | return nil, err
142 | }
143 |
144 | if resp2, err = dr.executeRequest(auth.toString()); err != nil {
145 | return nil, err
146 | }
147 |
148 | dr.Auth = auth
149 | return resp2, nil
150 | }
151 |
152 | func (dr *DigestRequest) executeExistingDigest() (resp *http.Response, err error) {
153 | var auth *authorization
154 |
155 | if auth, err = dr.Auth.refreshAuthorization(dr); err != nil {
156 | return nil, err
157 | }
158 | dr.Auth = auth
159 |
160 | return dr.executeRequest(dr.Auth.toString())
161 | }
162 |
163 | func (dr *DigestRequest) executeRequest(authString string) (resp *http.Response, err error) {
164 | var req *http.Request
165 |
166 | if req, err = http.NewRequest(dr.Method, dr.URI, bytes.NewReader([]byte(dr.Body))); err != nil {
167 | return nil, err
168 | }
169 | req.Header = dr.Header
170 | req.Header.Add("Authorization", authString)
171 |
172 | client := dr.getHTTPClient()
173 | return client.Do(req)
174 | }
175 |
--------------------------------------------------------------------------------
/tests/.htdigest:
--------------------------------------------------------------------------------
1 | test:Private:d75b886e0865b975d729deb19441ddde
2 |
--------------------------------------------------------------------------------
/tests/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.3"
2 | services:
3 | httpd:
4 | image: httpd:latest
5 | volumes:
6 | - ./httpd.conf:/usr/local/apache2/conf/httpd.conf:ro
7 | - ./.htdigest:/usr/local/apache2/conf/.htdigest:ro
8 | networks:
9 | default_net:
10 | ipv4_address: 172.16.1.5
11 | networks:
12 | default_net:
13 | driver: bridge
14 | ipam:
15 | driver: default
16 | config:
17 | - subnet: 172.16.1.1/27 # 1-30
18 |
--------------------------------------------------------------------------------
/tests/httpd.conf:
--------------------------------------------------------------------------------
1 | #
2 | # This is the main Apache HTTP server configuration file. It contains the
3 | # configuration directives that give the server its instructions.
4 | # See for detailed information.
5 | # In particular, see
6 | #
7 | # for a discussion of each configuration directive.
8 | #
9 | # Do NOT simply read the instructions in here without understanding
10 | # what they do. They're here only as hints or reminders. If you are unsure
11 | # consult the online docs. You have been warned.
12 | #
13 | # Configuration and logfile names: If the filenames you specify for many
14 | # of the server's control files begin with "/" (or "drive:/" for Win32), the
15 | # server will use that explicit path. If the filenames do *not* begin
16 | # with "/", the value of ServerRoot is prepended -- so "logs/access_log"
17 | # with ServerRoot set to "/usr/local/apache2" will be interpreted by the
18 | # server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log"
19 | # will be interpreted as '/logs/access_log'.
20 |
21 | #
22 | # ServerRoot: The top of the directory tree under which the server's
23 | # configuration, error, and log files are kept.
24 | #
25 | # Do not add a slash at the end of the directory path. If you point
26 | # ServerRoot at a non-local disk, be sure to specify a local disk on the
27 | # Mutex directive, if file-based mutexes are used. If you wish to share the
28 | # same ServerRoot for multiple httpd daemons, you will need to change at
29 | # least PidFile.
30 | #
31 | ServerRoot "/usr/local/apache2"
32 |
33 | #
34 | # Mutex: Allows you to set the mutex mechanism and mutex file directory
35 | # for individual mutexes, or change the global defaults
36 | #
37 | # Uncomment and change the directory if mutexes are file-based and the default
38 | # mutex file directory is not on a local disk or is not appropriate for some
39 | # other reason.
40 | #
41 | # Mutex default:logs
42 |
43 | #
44 | # Listen: Allows you to bind Apache to specific IP addresses and/or
45 | # ports, instead of the default. See also the
46 | # directive.
47 | #
48 | # Change this to Listen on specific IP addresses as shown below to
49 | # prevent Apache from glomming onto all bound IP addresses.
50 | #
51 | #Listen 12.34.56.78:80
52 | Listen 80
53 |
54 | #
55 | # Dynamic Shared Object (DSO) Support
56 | #
57 | # To be able to use the functionality of a module which was built as a DSO you
58 | # have to place corresponding `LoadModule' lines at this location so the
59 | # directives contained in it are actually available _before_ they are used.
60 | # Statically compiled modules (those listed by `httpd -l') do not need
61 | # to be loaded here.
62 | #
63 | # Example:
64 | # LoadModule foo_module modules/mod_foo.so
65 | #
66 | LoadModule authn_file_module modules/mod_authn_file.so
67 | #LoadModule authn_dbm_module modules/mod_authn_dbm.so
68 | #LoadModule authn_anon_module modules/mod_authn_anon.so
69 | #LoadModule authn_dbd_module modules/mod_authn_dbd.so
70 | #LoadModule authn_socache_module modules/mod_authn_socache.so
71 | LoadModule authn_core_module modules/mod_authn_core.so
72 | LoadModule authz_host_module modules/mod_authz_host.so
73 | LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
74 | LoadModule authz_user_module modules/mod_authz_user.so
75 | #LoadModule authz_dbm_module modules/mod_authz_dbm.so
76 | #LoadModule authz_owner_module modules/mod_authz_owner.so
77 | #LoadModule authz_dbd_module modules/mod_authz_dbd.so
78 | LoadModule authz_core_module modules/mod_authz_core.so
79 | #LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
80 | #LoadModule authnz_fcgi_module modules/mod_authnz_fcgi.so
81 | LoadModule access_compat_module modules/mod_access_compat.so
82 | LoadModule auth_basic_module modules/mod_auth_basic.so
83 | #LoadModule auth_form_module modules/mod_auth_form.so
84 | LoadModule auth_digest_module modules/mod_auth_digest.so
85 | #LoadModule allowmethods_module modules/mod_allowmethods.so
86 | #LoadModule isapi_module modules/mod_isapi.so
87 | #LoadModule file_cache_module modules/mod_file_cache.so
88 | #LoadModule cache_module modules/mod_cache.so
89 | #LoadModule cache_disk_module modules/mod_cache_disk.so
90 | #LoadModule cache_socache_module modules/mod_cache_socache.so
91 | #LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
92 | #LoadModule socache_dbm_module modules/mod_socache_dbm.so
93 | #LoadModule socache_memcache_module modules/mod_socache_memcache.so
94 | #LoadModule watchdog_module modules/mod_watchdog.so
95 | #LoadModule macro_module modules/mod_macro.so
96 | #LoadModule dbd_module modules/mod_dbd.so
97 | #LoadModule bucketeer_module modules/mod_bucketeer.so
98 | #LoadModule dumpio_module modules/mod_dumpio.so
99 | #LoadModule echo_module modules/mod_echo.so
100 | #LoadModule example_hooks_module modules/mod_example_hooks.so
101 | #LoadModule case_filter_module modules/mod_case_filter.so
102 | #LoadModule case_filter_in_module modules/mod_case_filter_in.so
103 | #LoadModule example_ipc_module modules/mod_example_ipc.so
104 | #LoadModule buffer_module modules/mod_buffer.so
105 | #LoadModule data_module modules/mod_data.so
106 | #LoadModule ratelimit_module modules/mod_ratelimit.so
107 | LoadModule reqtimeout_module modules/mod_reqtimeout.so
108 | #LoadModule ext_filter_module modules/mod_ext_filter.so
109 | #LoadModule request_module modules/mod_request.so
110 | #LoadModule include_module modules/mod_include.so
111 | LoadModule filter_module modules/mod_filter.so
112 | #LoadModule reflector_module modules/mod_reflector.so
113 | #LoadModule substitute_module modules/mod_substitute.so
114 | #LoadModule sed_module modules/mod_sed.so
115 | #LoadModule charset_lite_module modules/mod_charset_lite.so
116 | #LoadModule deflate_module modules/mod_deflate.so
117 | #LoadModule xml2enc_module modules/mod_xml2enc.so
118 | #LoadModule proxy_html_module modules/mod_proxy_html.so
119 | LoadModule mime_module modules/mod_mime.so
120 | #LoadModule ldap_module modules/mod_ldap.so
121 | LoadModule log_config_module modules/mod_log_config.so
122 | #LoadModule log_debug_module modules/mod_log_debug.so
123 | #LoadModule log_forensic_module modules/mod_log_forensic.so
124 | #LoadModule logio_module modules/mod_logio.so
125 | #LoadModule lua_module modules/mod_lua.so
126 | LoadModule env_module modules/mod_env.so
127 | #LoadModule mime_magic_module modules/mod_mime_magic.so
128 | #LoadModule cern_meta_module modules/mod_cern_meta.so
129 | #LoadModule expires_module modules/mod_expires.so
130 | LoadModule headers_module modules/mod_headers.so
131 | #LoadModule ident_module modules/mod_ident.so
132 | #LoadModule usertrack_module modules/mod_usertrack.so
133 | #LoadModule unique_id_module modules/mod_unique_id.so
134 | LoadModule setenvif_module modules/mod_setenvif.so
135 | LoadModule version_module modules/mod_version.so
136 | #LoadModule remoteip_module modules/mod_remoteip.so
137 | #LoadModule proxy_module modules/mod_proxy.so
138 | #LoadModule proxy_connect_module modules/mod_proxy_connect.so
139 | #LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
140 | #LoadModule proxy_http_module modules/mod_proxy_http.so
141 | #LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
142 | #LoadModule proxy_scgi_module modules/mod_proxy_scgi.so
143 | #LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so
144 | #LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so
145 | #LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
146 | #LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
147 | #LoadModule proxy_express_module modules/mod_proxy_express.so
148 | #LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so
149 | #LoadModule session_module modules/mod_session.so
150 | #LoadModule session_cookie_module modules/mod_session_cookie.so
151 | #LoadModule session_crypto_module modules/mod_session_crypto.so
152 | #LoadModule session_dbd_module modules/mod_session_dbd.so
153 | #LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
154 | #LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
155 | #LoadModule ssl_module modules/mod_ssl.so
156 | #LoadModule optional_hook_export_module modules/mod_optional_hook_export.so
157 | #LoadModule optional_hook_import_module modules/mod_optional_hook_import.so
158 | #LoadModule optional_fn_import_module modules/mod_optional_fn_import.so
159 | #LoadModule optional_fn_export_module modules/mod_optional_fn_export.so
160 | #LoadModule dialup_module modules/mod_dialup.so
161 | #LoadModule http2_module modules/mod_http2.so
162 | #LoadModule proxy_http2_module modules/mod_proxy_http2.so
163 | #LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
164 | #LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
165 | #LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
166 | #LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so
167 | LoadModule unixd_module modules/mod_unixd.so
168 | #LoadModule heartbeat_module modules/mod_heartbeat.so
169 | #LoadModule heartmonitor_module modules/mod_heartmonitor.so
170 | #LoadModule dav_module modules/mod_dav.so
171 | LoadModule status_module modules/mod_status.so
172 | LoadModule autoindex_module modules/mod_autoindex.so
173 | #LoadModule asis_module modules/mod_asis.so
174 | #LoadModule info_module modules/mod_info.so
175 | #LoadModule suexec_module modules/mod_suexec.so
176 |
177 | #LoadModule cgid_module modules/mod_cgid.so
178 |
179 |
180 | #LoadModule cgi_module modules/mod_cgi.so
181 |
182 | #LoadModule dav_fs_module modules/mod_dav_fs.so
183 | #LoadModule dav_lock_module modules/mod_dav_lock.so
184 | #LoadModule vhost_alias_module modules/mod_vhost_alias.so
185 | #LoadModule negotiation_module modules/mod_negotiation.so
186 | LoadModule dir_module modules/mod_dir.so
187 | #LoadModule imagemap_module modules/mod_imagemap.so
188 | #LoadModule actions_module modules/mod_actions.so
189 | #LoadModule speling_module modules/mod_speling.so
190 | #LoadModule userdir_module modules/mod_userdir.so
191 | LoadModule alias_module modules/mod_alias.so
192 | #LoadModule rewrite_module modules/mod_rewrite.so
193 |
194 |
195 | #
196 | # If you wish httpd to run as a different user or group, you must run
197 | # httpd as root initially and it will switch.
198 | #
199 | # User/Group: The name (or #number) of the user/group to run httpd as.
200 | # It is usually good practice to create a dedicated user and group for
201 | # running httpd, as with most system services.
202 | #
203 | User daemon
204 | Group daemon
205 |
206 |
207 |
208 | # 'Main' server configuration
209 | #
210 | # The directives in this section set up the values used by the 'main'
211 | # server, which responds to any requests that aren't handled by a
212 | # definition. These values also provide defaults for
213 | # any containers you may define later in the file.
214 | #
215 | # All of these directives may appear inside containers,
216 | # in which case these default settings will be overridden for the
217 | # virtual host being defined.
218 | #
219 |
220 | #
221 | # ServerAdmin: Your address, where problems with the server should be
222 | # e-mailed. This address appears on some server-generated pages, such
223 | # as error documents. e.g. admin@your-domain.com
224 | #
225 | ServerAdmin you@example.com
226 |
227 | #
228 | # ServerName gives the name and port that the server uses to identify itself.
229 | # This can often be determined automatically, but we recommend you specify
230 | # it explicitly to prevent problems during startup.
231 | #
232 | # If your host doesn't have a registered DNS name, enter its IP address here.
233 | #
234 | #ServerName www.example.com:80
235 |
236 | #
237 | # Deny access to the entirety of your server's filesystem. You must
238 | # explicitly permit access to web content directories in other
239 | # blocks below.
240 | #
241 |
242 | AllowOverride none
243 | Require all denied
244 |
245 |
246 | #
247 | # Note that from this point forward you must specifically allow
248 | # particular features to be enabled - so if something's not working as
249 | # you might expect, make sure that you have specifically enabled it
250 | # below.
251 | #
252 |
253 | #
254 | # DocumentRoot: The directory out of which you will serve your
255 | # documents. By default, all requests are taken from this directory, but
256 | # symbolic links and aliases may be used to point to other locations.
257 | #
258 | DocumentRoot "/usr/local/apache2/htdocs"
259 |
260 | #
261 | # Possible values for the Options directive are "None", "All",
262 | # or any combination of:
263 | # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
264 | #
265 | # Note that "MultiViews" must be named *explicitly* --- "Options All"
266 | # doesn't give it to you.
267 | #
268 | # The Options directive is both complicated and important. Please see
269 | # http://httpd.apache.org/docs/2.4/mod/core.html#options
270 | # for more information.
271 | #
272 | Options Indexes FollowSymLinks
273 |
274 | #
275 | # AllowOverride controls what directives may be placed in .htaccess files.
276 | # It can be "All", "None", or any combination of the keywords:
277 | # AllowOverride FileInfo AuthConfig Limit
278 | #
279 | AllowOverride None
280 |
281 | #
282 | # Controls who can get stuff from this server.
283 | #
284 | Require all granted
285 |
286 |
287 | #
288 | # DirectoryIndex: sets the file that Apache will serve if a directory
289 | # is requested.
290 | #
291 |
292 | DirectoryIndex index.html
293 |
294 |
295 | #
296 | # The following lines prevent .htaccess and .htpasswd files from being
297 | # viewed by Web clients.
298 | #
299 |
300 | Require all denied
301 |
302 |
303 | #
304 | # ErrorLog: The location of the error log file.
305 | # If you do not specify an ErrorLog directive within a
306 | # container, error messages relating to that virtual host will be
307 | # logged here. If you *do* define an error logfile for a
308 | # container, that host's errors will be logged there and not here.
309 | #
310 | ErrorLog /proc/self/fd/2
311 |
312 | #
313 | # LogLevel: Control the number of messages logged to the error_log.
314 | # Possible values include: debug, info, notice, warn, error, crit,
315 | # alert, emerg.
316 | #
317 | LogLevel warn
318 |
319 |
320 | #
321 | # The following directives define some format nicknames for use with
322 | # a CustomLog directive (see below).
323 | #
324 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
325 | LogFormat "%h %l %u %t \"%r\" %>s %b" common
326 |
327 |
328 | # You need to enable mod_logio.c to use %I and %O
329 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
330 |
331 |
332 | #
333 | # The location and format of the access logfile (Common Logfile Format).
334 | # If you do not define any access logfiles within a
335 | # container, they will be logged here. Contrariwise, if you *do*
336 | # define per- access logfiles, transactions will be
337 | # logged therein and *not* in this file.
338 | #
339 | CustomLog /proc/self/fd/1 common
340 |
341 | #
342 | # If you prefer a logfile with access, agent, and referer information
343 | # (Combined Logfile Format) you can use the following directive.
344 | #
345 | #CustomLog "logs/access_log" combined
346 |
347 |
348 |
349 | #
350 | # Redirect: Allows you to tell clients about documents that used to
351 | # exist in your server's namespace, but do not anymore. The client
352 | # will make a new request for the document at its new location.
353 | # Example:
354 | # Redirect permanent /foo http://www.example.com/bar
355 |
356 | #
357 | # Alias: Maps web paths into filesystem paths and is used to
358 | # access content that does not live under the DocumentRoot.
359 | # Example:
360 | # Alias /webpath /full/filesystem/path
361 | #
362 | # If you include a trailing / on /webpath then the server will
363 | # require it to be present in the URL. You will also likely
364 | # need to provide a section to allow access to
365 | # the filesystem path.
366 |
367 | #
368 | # ScriptAlias: This controls which directories contain server scripts.
369 | # ScriptAliases are essentially the same as Aliases, except that
370 | # documents in the target directory are treated as applications and
371 | # run by the server when requested rather than as documents sent to the
372 | # client. The same rules about trailing "/" apply to ScriptAlias
373 | # directives as to Alias.
374 | #
375 | ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
376 |
377 |
378 |
379 |
380 | #
381 | # ScriptSock: On threaded servers, designate the path to the UNIX
382 | # socket used to communicate with the CGI daemon of mod_cgid.
383 | #
384 | #Scriptsock cgisock
385 |
386 |
387 | #
388 | # "/usr/local/apache2/cgi-bin" should be changed to whatever your ScriptAliased
389 | # CGI directory exists, if you have that configured.
390 | #
391 |
392 | AllowOverride None
393 | Options None
394 | Require all granted
395 |
396 |
397 |
398 | #
399 | # Avoid passing HTTP_PROXY environment to CGI's on this or any proxied
400 | # backend servers which have lingering "httpoxy" defects.
401 | # 'Proxy' request header is undefined by the IETF, not listed by IANA
402 | #
403 | RequestHeader unset Proxy early
404 |
405 |
406 |
407 | #
408 | # TypesConfig points to the file containing the list of mappings from
409 | # filename extension to MIME-type.
410 | #
411 | TypesConfig conf/mime.types
412 |
413 | #
414 | # AddType allows you to add to or override the MIME configuration
415 | # file specified in TypesConfig for specific file types.
416 | #
417 | #AddType application/x-gzip .tgz
418 | #
419 | # AddEncoding allows you to have certain browsers uncompress
420 | # information on the fly. Note: Not all browsers support this.
421 | #
422 | #AddEncoding x-compress .Z
423 | #AddEncoding x-gzip .gz .tgz
424 | #
425 | # If the AddEncoding directives above are commented-out, then you
426 | # probably should define those extensions to indicate media types:
427 | #
428 | AddType application/x-compress .Z
429 | AddType application/x-gzip .gz .tgz
430 |
431 | #
432 | # AddHandler allows you to map certain file extensions to "handlers":
433 | # actions unrelated to filetype. These can be either built into the server
434 | # or added with the Action directive (see below)
435 | #
436 | # To use CGI scripts outside of ScriptAliased directories:
437 | # (You will also need to add "ExecCGI" to the "Options" directive.)
438 | #
439 | #AddHandler cgi-script .cgi
440 |
441 | # For type maps (negotiated resources):
442 | #AddHandler type-map var
443 |
444 | #
445 | # Filters allow you to process content before it is sent to the client.
446 | #
447 | # To parse .shtml files for server-side includes (SSI):
448 | # (You will also need to add "Includes" to the "Options" directive.)
449 | #
450 | #AddType text/html .shtml
451 | #AddOutputFilter INCLUDES .shtml
452 |
453 |
454 | #
455 | # The mod_mime_magic module allows the server to use various hints from the
456 | # contents of the file itself to determine its type. The MIMEMagicFile
457 | # directive tells the module where the hint definitions are located.
458 | #
459 | #MIMEMagicFile conf/magic
460 |
461 | #
462 | # Customizable error responses come in three flavors:
463 | # 1) plain text 2) local redirects 3) external redirects
464 | #
465 | # Some examples:
466 | #ErrorDocument 500 "The server made a boo boo."
467 | #ErrorDocument 404 /missing.html
468 | #ErrorDocument 404 "/cgi-bin/missing_handler.pl"
469 | #ErrorDocument 402 http://www.example.com/subscription_info.html
470 | #
471 |
472 | #
473 | # MaxRanges: Maximum number of Ranges in a request before
474 | # returning the entire resource, or one of the special
475 | # values 'default', 'none' or 'unlimited'.
476 | # Default setting is to accept 200 Ranges.
477 | #MaxRanges unlimited
478 |
479 | #
480 | # EnableMMAP and EnableSendfile: On systems that support it,
481 | # memory-mapping or the sendfile syscall may be used to deliver
482 | # files. This usually improves server performance, but must
483 | # be turned off when serving from networked-mounted
484 | # filesystems or if support for these functions is otherwise
485 | # broken on your system.
486 | # Defaults: EnableMMAP On, EnableSendfile Off
487 | #
488 | #EnableMMAP off
489 | #EnableSendfile on
490 |
491 | # Supplemental configuration
492 | #
493 | # The configuration files in the conf/extra/ directory can be
494 | # included to add extra features or to modify the default configuration of
495 | # the server, or you may simply copy their contents here and change as
496 | # necessary.
497 |
498 | # Server-pool management (MPM specific)
499 | #Include conf/extra/httpd-mpm.conf
500 |
501 | # Multi-language error messages
502 | #Include conf/extra/httpd-multilang-errordoc.conf
503 |
504 | # Fancy directory listings
505 | #Include conf/extra/httpd-autoindex.conf
506 |
507 | # Language settings
508 | #Include conf/extra/httpd-languages.conf
509 |
510 | # User home directories
511 | #Include conf/extra/httpd-userdir.conf
512 |
513 | # Real-time info on requests and configuration
514 | #Include conf/extra/httpd-info.conf
515 |
516 | # Virtual hosts
517 | #Include conf/extra/httpd-vhosts.conf
518 |
519 | # Local access to the Apache HTTP Server Manual
520 | #Include conf/extra/httpd-manual.conf
521 |
522 | # Distributed authoring and versioning (WebDAV)
523 | #Include conf/extra/httpd-dav.conf
524 |
525 | # Various default settings
526 | #Include conf/extra/httpd-default.conf
527 |
528 | # Configure mod_proxy_html to understand HTML4/XHTML1
529 |
530 | Include conf/extra/proxy-html.conf
531 |
532 |
533 | # Secure (SSL/TLS) connections
534 | #Include conf/extra/httpd-ssl.conf
535 | #
536 | # Note: The following must must be present to support
537 | # starting without SSL on platforms with no /dev/random equivalent
538 | # but a statically compiled-in mod_ssl.
539 | #
540 |
541 | SSLRandomSeed startup builtin
542 | SSLRandomSeed connect builtin
543 |
544 |
545 |
546 |
547 | AuthType Digest
548 | AuthName "Private"
549 | AuthDigestDomain /
550 | AuthDigestProvider file
551 | AuthUserFile "/usr/local/apache2/conf/.htdigest"
552 | Require valid-user
553 |
554 |
--------------------------------------------------------------------------------
/tests/manual-test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net/http"
8 |
9 | dac "github.com/xinsnake/go-http-digest-auth-client"
10 | )
11 |
12 | const (
13 | username = "test"
14 | password = "test123"
15 | method = "GET"
16 | uri = "http://172.16.1.5"
17 | )
18 |
19 | func main() {
20 | var resp *http.Response
21 | var body []byte
22 | var err error
23 |
24 | dr := dac.NewRequest(username, password, method, uri, "")
25 |
26 | if resp, err = dr.Execute(); err != nil {
27 | log.Fatalln(err)
28 | }
29 | defer resp.Body.Close()
30 |
31 | if body, err = ioutil.ReadAll(resp.Body); err != nil {
32 | log.Fatalln(err)
33 | }
34 |
35 | fmt.Printf(string(body))
36 | }
37 |
--------------------------------------------------------------------------------
/www_authenticate.go:
--------------------------------------------------------------------------------
1 | package digest_auth_client
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 | )
7 |
8 | type wwwAuthenticate struct {
9 | Algorithm string // unquoted
10 | Domain string // quoted
11 | Nonce string // quoted
12 | Opaque string // quoted
13 | Qop string // quoted
14 | Realm string // quoted
15 | Stale bool // unquoted
16 | Charset string // quoted
17 | Userhash bool // quoted
18 | }
19 |
20 | func newWwwAuthenticate(s string) *wwwAuthenticate {
21 |
22 | var wa = wwwAuthenticate{}
23 |
24 | algorithmRegex := regexp.MustCompile(`algorithm="([^ ,]+)"`)
25 | algorithmMatch := algorithmRegex.FindStringSubmatch(s)
26 | if algorithmMatch != nil {
27 | wa.Algorithm = algorithmMatch[1]
28 | }
29 |
30 | domainRegex := regexp.MustCompile(`domain="(.+?)"`)
31 | domainMatch := domainRegex.FindStringSubmatch(s)
32 | if domainMatch != nil {
33 | wa.Domain = domainMatch[1]
34 | }
35 |
36 | nonceRegex := regexp.MustCompile(`nonce="(.+?)"`)
37 | nonceMatch := nonceRegex.FindStringSubmatch(s)
38 | if nonceMatch != nil {
39 | wa.Nonce = nonceMatch[1]
40 | }
41 |
42 | opaqueRegex := regexp.MustCompile(`opaque="(.+?)"`)
43 | opaqueMatch := opaqueRegex.FindStringSubmatch(s)
44 | if opaqueMatch != nil {
45 | wa.Opaque = opaqueMatch[1]
46 | }
47 |
48 | qopRegex := regexp.MustCompile(`qop="(.+?)"`)
49 | qopMatch := qopRegex.FindStringSubmatch(s)
50 | if qopMatch != nil {
51 | wa.Qop = qopMatch[1]
52 | }
53 |
54 | realmRegex := regexp.MustCompile(`realm="(.+?)"`)
55 | realmMatch := realmRegex.FindStringSubmatch(s)
56 | if realmMatch != nil {
57 | wa.Realm = realmMatch[1]
58 | }
59 |
60 | staleRegex := regexp.MustCompile(`stale=([^ ,])"`)
61 | staleMatch := staleRegex.FindStringSubmatch(s)
62 | if staleMatch != nil {
63 | wa.Stale = (strings.ToLower(staleMatch[1]) == "true")
64 | }
65 |
66 | charsetRegex := regexp.MustCompile(`charset="(.+?)"`)
67 | charsetMatch := charsetRegex.FindStringSubmatch(s)
68 | if charsetMatch != nil {
69 | wa.Charset = charsetMatch[1]
70 | }
71 |
72 | userhashRegex := regexp.MustCompile(`userhash=([^ ,])"`)
73 | userhashMatch := userhashRegex.FindStringSubmatch(s)
74 | if userhashMatch != nil {
75 | wa.Userhash = (strings.ToLower(userhashMatch[1]) == "true")
76 | }
77 |
78 | return &wa
79 | }
80 |
--------------------------------------------------------------------------------