├── 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 | --------------------------------------------------------------------------------