github.com/go-kivik/kivik/v4@v4.3.2/couchdb/test/session.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 package test 14 15 import ( 16 "context" 17 "encoding/base64" 18 "encoding/hex" 19 "encoding/json" 20 "fmt" 21 "net/http" 22 "net/url" 23 "strings" 24 25 kivik "github.com/go-kivik/kivik/v4" 26 "github.com/go-kivik/kivik/v4/couchdb/chttp" 27 "github.com/go-kivik/kivik/v4/int/mock" 28 "github.com/go-kivik/kivik/v4/kiviktest/kt" 29 ) 30 31 func init() { 32 kt.Register("Session", session) 33 } 34 35 func session(ctx *kt.Context) { 36 chttpAdmin, err := chttp.New(&http.Client{}, ctx.Admin.DSN(), mock.NilOption) 37 if err != nil { 38 ctx.Fatalf("chttp.Admin failed: %s", err) 39 } 40 chttpNoAuth, err := chttp.New(&http.Client{}, ctx.NoAuth.DSN(), mock.NilOption) 41 if err != nil { 42 ctx.Fatalf("chttp.NoAuth failed: %s", err) 43 } 44 ctx.Run("Get", func(ctx *kt.Context) { 45 ctx.RunAdmin(func(ctx *kt.Context) { 46 testSession(ctx, chttpAdmin) 47 }) 48 ctx.RunNoAuth(func(ctx *kt.Context) { 49 testSession(ctx, chttpNoAuth) 50 }) 51 }) 52 ctx.Run("Post", func(ctx *kt.Context) { 53 testCreateSession(ctx, chttpNoAuth) 54 }) 55 ctx.Run("Delete", func(ctx *kt.Context) { 56 testDeleteSession(ctx, chttpNoAuth) 57 }) 58 } 59 60 func testSession(ctx *kt.Context, client *chttp.Client) { 61 ctx.Parallel() 62 if client == nil { 63 ctx.Skipf("No CHTTP client") 64 } 65 uCtx := struct { 66 Info struct { 67 AuthMethod string `json:"authenticated"` 68 AuthDB string `json:"authentication_db"` 69 AuthHandlers []string `json:"authentication_handlers"` 70 } `json:"info"` 71 OK bool `json:"ok"` 72 UserCtx struct { 73 Name string `json:"name"` 74 Roles []string `json:"roles"` 75 } `json:"userCtx"` 76 }{} 77 err := client.DoJSON(context.Background(), http.MethodGet, "/_session", nil, &uCtx) 78 if !ctx.IsExpectedSuccess(err) { 79 return 80 } 81 values := map[string]string{ 82 "info.authenticated": uCtx.Info.AuthMethod, 83 "info.authentication_db": uCtx.Info.AuthDB, 84 "info.authentication_handlers": strings.Join(uCtx.Info.AuthHandlers, ","), 85 "ok": fmt.Sprintf("%t", uCtx.OK), 86 "userCtx.roles": strings.Join(uCtx.UserCtx.Roles, ","), 87 } 88 for key, actual := range values { 89 expected := ctx.MustString(key) 90 if actual != expected { 91 ctx.Errorf("Unexpected value for `%s`. Expected '%s', actual '%s'", key, expected, actual) 92 } 93 } 94 dsn, _ := url.Parse(client.DSN()) 95 var expected string 96 if dsn.User != nil { 97 expected = dsn.User.Username() 98 } 99 actual := uCtx.UserCtx.Name 100 if actual != expected { 101 ctx.Errorf("Unexpected value for `%s`. Expected '%s', actual '%s'", "userCtx.name", expected, actual) 102 } 103 } 104 105 type sessionPostTest struct { 106 Name string 107 Query string 108 Options *chttp.Options 109 // True if the test requires valid credentials 110 Creds bool 111 } 112 113 func testCreateSession(ctx *kt.Context, client *chttp.Client) { 114 if client == nil { 115 ctx.Skipf("No CHTTP client") 116 } 117 // Re-create client, so we can override defaults 118 client, _ = chttp.New(&http.Client{}, client.DSN(), mock.NilOption) 119 // Don't follow redirect 120 client.CheckRedirect = func(*http.Request, []*http.Request) error { 121 return http.ErrUseLastResponse 122 } 123 var name, password string 124 if ctx.Admin != nil { 125 if dsn, _ := url.Parse(ctx.Admin.DSN()); dsn.User != nil { 126 name = dsn.User.Username() 127 password, _ = dsn.User.Password() 128 } 129 } 130 tests := []sessionPostTest{ 131 {Name: "EmptyJSON", Options: &chttp.Options{ContentType: "application/json"}}, 132 {Name: "BadJSON", Options: &chttp.Options{ 133 ContentType: "application/json", 134 Body: kt.Body("oink"), 135 }}, 136 {Name: "BogusTypeJSON", Creds: true, Options: &chttp.Options{ 137 ContentType: "image/gif", 138 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 139 }}, 140 {Name: "BogusTypeForm", Creds: true, Options: &chttp.Options{ 141 ContentType: "image/gif", 142 Body: kt.Body(`name=%s&password=%s`, name, password), 143 }}, 144 {Name: "EmptyForm", Options: &chttp.Options{ContentType: "application/x-www-form-urlencoded"}}, 145 {Name: "BadForm", Options: &chttp.Options{ 146 ContentType: "application/x-www-form-urlencoded", 147 Body: kt.Body("o\\ink"), 148 }}, 149 {Name: "MeaninglessJSON", Options: &chttp.Options{ 150 ContentType: "application/json", 151 Body: kt.Body(`{"ok":true}`), 152 }}, 153 {Name: "MeaninglessForm", Options: &chttp.Options{ 154 ContentType: "application/x-www-form-urlencoded", 155 Body: kt.Body("ok=true"), 156 }}, 157 {Name: "GoodJSON", Options: &chttp.Options{ 158 ContentType: "application/json", 159 Body: kt.Body(`{"name":"bob","password":"abc123"}`), 160 }}, 161 {Name: "BadQueryParam", Query: "foobarbaz!", Options: &chttp.Options{ 162 ContentType: "application/json", 163 Body: kt.Body(`{"name":"bob","password":"abc123"}`), 164 }}, 165 {Name: "GoodCredsJSON", Creds: true, Options: &chttp.Options{ 166 ContentType: "application/json", 167 Body: kt.Body(fmt.Sprintf(`{"name":"%s","password":"%s"}`, name, password)), 168 }}, 169 {Name: "GoodCredsForm", Creds: true, Options: &chttp.Options{ 170 ContentType: "application/x-www-form-urlencoded", 171 Body: kt.Body(fmt.Sprintf(`name=%s&password=%s`, name, password)), 172 }}, 173 {Name: "BadCredsJSON", Creds: true, Options: &chttp.Options{ 174 ContentType: "application/json", 175 Body: kt.Body(fmt.Sprintf(`{"name":"%s","password":"%sxxx"}`, name, password)), 176 }}, 177 {Name: "BadCredsForm", Creds: true, Options: &chttp.Options{ 178 ContentType: "application/x-www-form-urlencoded", 179 Body: kt.Body(`name=%s&password=%sxxx`, name, password), 180 }}, 181 {Name: "GoodCredsJSONRedirEmpty", Creds: true, Query: "next=", Options: &chttp.Options{ 182 ContentType: "application/json", 183 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 184 }}, 185 {Name: "GoodCredsJSONRedirRelative", Creds: true, Query: "next=/_session", Options: &chttp.Options{ 186 ContentType: "application/json", 187 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 188 }}, 189 {Name: "GoodCredsJSONRedirSchemaless", Creds: true, Query: "next=//_session", Options: &chttp.Options{ 190 ContentType: "application/json", 191 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 192 }}, 193 {Name: "GoodCredsJSONRedirRelativeNoSlash", Creds: true, Query: "next=foobar", Options: &chttp.Options{ 194 ContentType: "application/json", 195 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 196 }}, 197 {Name: "GoodCredsJSONRemoteRedirAbsolute", Creds: true, Query: "next=http://google.com/", Options: &chttp.Options{ 198 ContentType: "application/json", 199 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 200 }}, 201 {Name: "GoodCredsJSONRemoteRedirInvalidURL", Creds: true, Query: "next=/session%25%26%26", Options: &chttp.Options{ 202 ContentType: "application/json", 203 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 204 }}, 205 {Name: "GoodCredsJSONRemoteRedirHeaderInjection", Creds: true, Query: "next=/foo\nX-Injected: oink", Options: &chttp.Options{ 206 ContentType: "application/json", 207 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 208 }}, 209 {Name: "AcceptPlain", Creds: true, Options: &chttp.Options{ 210 ContentType: "application/json", Accept: "text/plain", 211 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 212 }}, 213 {Name: "AcceptImage", Creds: true, Options: &chttp.Options{ 214 ContentType: "application/json", Accept: "image/gif", 215 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 216 }}, 217 } 218 for _, postTest := range tests { 219 func(test sessionPostTest) { 220 ctx.Run(test.Name, func(ctx *kt.Context) { 221 if test.Creds && name == "" { 222 ctx.Skipf("Credentials required but missing, skipping test.") 223 } 224 ctx.Parallel() 225 reqURL := "/_session" 226 if test.Query != "" { 227 reqURL += "?" + test.Query 228 } 229 r, err := client.DoReq(context.Background(), http.MethodPost, reqURL, test.Options) 230 if err == nil { 231 err = chttp.ResponseError(r) 232 } 233 if !ctx.IsExpectedSuccess(err) { 234 return 235 } 236 defer r.Body.Close() // nolint: errcheck 237 if _, ok := r.Header["Cache-Control"]; !ok { 238 ctx.Errorf("No Cache-Control set in response.") 239 } else { 240 cc := r.Header.Get("Cache-Control") 241 if strings.ToLower(cc) != "must-revalidate" { 242 ctx.Errorf("Expected Cache-Control: must-revalidate, but got'%s", cc) 243 } 244 } 245 if strings.HasPrefix(test.Query, "next=") { 246 if r.StatusCode != http.StatusFound { 247 ctx.Errorf("Expected redirect") 248 } else { 249 q, _ := url.ParseQuery(test.Query) 250 loc := r.Header.Get("Location") 251 next := q.Get("next") 252 if !strings.HasSuffix(loc, next) { 253 ctx.Errorf("Expected Location: ...%s, got: %s", next, loc) 254 } 255 } 256 } 257 cookies := r.Cookies() 258 if len(cookies) != 1 { 259 ctx.Errorf("Expected 1 cookie, got %d", len(cookies)) 260 } 261 if cookies[0].Name != kivik.SessionCookieName { 262 ctx.Errorf("Server set cookie '%s', expected '%s'", cookies[0].Name, kivik.SessionCookieName) 263 } 264 if !cookies[0].HttpOnly { 265 ctx.Errorf("Cookie is not set HttpOnly") 266 } 267 if cookies[0].Path != "/" { 268 ctx.Errorf("Unexpected cookie path. Got '%s', expected '/'", cookies[0].Path) 269 } 270 val, err := base64.RawURLEncoding.DecodeString(cookies[0].Value) 271 if err != nil { 272 ctx.Fatalf("Failed to decode cookie value: %s", err) 273 } 274 parts := strings.SplitN(string(val), ":", 3) // nolint:gomnd 275 if parts[0] != name { 276 ctx.Errorf("Cookie does not match username. Want '%s', got '%s'", name, parts[0]) 277 } 278 if _, err := hex.DecodeString(parts[1]); err != nil { 279 ctx.Errorf("Failed to decode cookie timestamp: %s", err) 280 } 281 response := struct { 282 OK bool `json:"ok"` 283 Name *string `json:"name"` 284 Roles []string `json:"roles"` 285 }{} 286 if err := json.NewDecoder(r.Body).Decode(&response); err != nil { 287 ctx.Fatalf("Failed to decode response: %s", err) 288 } 289 if !response.OK { 290 ctx.Errorf("Expected OK response") 291 } 292 if response.Name != nil && *response.Name != name { 293 ctx.Errorf("Unexpected name in response. Expected '%s', got '%s'", name, *response.Name) 294 } 295 }) 296 }(postTest) 297 } 298 } 299 300 type deleteSessionTest struct { 301 Name string 302 Creds bool 303 Cookie *http.Cookie 304 } 305 306 func testDeleteSession(ctx *kt.Context, client *chttp.Client) { 307 ctx.Parallel() 308 if client == nil { 309 ctx.Skipf("No CHTTP client") 310 } 311 // Re-create client, so we can override defaults 312 client, _ = chttp.New(&http.Client{}, client.DSN(), mock.NilOption) 313 // Don't save sessions 314 client.Jar = nil 315 var cookie *http.Cookie 316 if ctx.Admin != nil { 317 if dsn, _ := url.Parse(ctx.Admin.DSN()); dsn.User != nil { 318 name := dsn.User.Username() 319 password, _ := dsn.User.Password() 320 r, err := client.DoReq(context.Background(), http.MethodPost, "/_session", &chttp.Options{ 321 Body: kt.Body(`{"name":"%s","password":"%s"}`, name, password), 322 }) 323 if err != nil { 324 ctx.Errorf("Failed to establish session: %s", err) 325 return 326 } 327 for _, c := range r.Cookies() { 328 if c.Name == kivik.SessionCookieName { 329 cookie = c 330 break 331 } 332 } 333 } 334 } 335 tests := []deleteSessionTest{ 336 {Name: "ValidSession", Creds: true, Cookie: cookie}, 337 {Name: "NoSession"}, 338 // {Name: "InvalidSession"}, 339 // {Name: "ExpiredSession"}, 340 } 341 for _, test := range tests { 342 func(test deleteSessionTest) { 343 ctx.Run(test.Name, func(ctx *kt.Context) { 344 if test.Creds && cookie == nil { 345 ctx.Skipf("Credentials required but missing, skipping test.") 346 } 347 response := struct { 348 OK bool `json:"ok"` 349 }{} 350 req, err := client.NewRequest(context.Background(), http.MethodDelete, "/_session", nil, nil) 351 if err != nil { 352 ctx.Fatalf("Failed to create request: %s", err) 353 } 354 if test.Cookie != nil { 355 req.AddCookie(test.Cookie) 356 } 357 r, err := client.Do(req) 358 if err == nil { 359 err = chttp.ResponseError(r) 360 } 361 if err == nil { 362 defer r.Body.Close() // nolint: errcheck 363 err = json.NewDecoder(r.Body).Decode(&response) 364 } 365 if !ctx.IsExpectedSuccess(err) { 366 return 367 } 368 if _, ok := r.Header["Cache-Control"]; !ok { 369 ctx.Errorf("No Cache-Control set in response.") 370 } else { 371 cc := r.Header.Get("Cache-Control") 372 if strings.ToLower(cc) != "must-revalidate" { 373 ctx.Errorf("Expected Cache-Control: must-revalidate, but got'%s", cc) 374 } 375 } 376 for _, c := range r.Cookies() { 377 if c.Name == kivik.SessionCookieName { 378 if c.Value != "" { 379 ctx.Errorf("Expected empty cookie value, got '%s'", c.Value) 380 } 381 break 382 } 383 } 384 }) 385 }(test) 386 } 387 }