├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── session │ ├── continuity │ └── RefreshableSession.java │ ├── csrf │ └── CsrfProtection.java │ ├── data_types │ ├── CustomType.java │ ├── CustomTypeSession.java │ ├── LongTypeSession.java │ ├── MapTypeSession.java │ └── StringTypeSession.java │ ├── directives │ ├── OptionalSessionDirective.java │ ├── SessionDirective.java │ └── TouchRequiredSessionDirective.java │ ├── jwt │ └── JwtEncodedSession.java │ └── transport │ ├── CookieTransport.java │ └── HeaderTransport.java └── resources ├── application.conf ├── index.html ├── jquery.min.js ├── js-cookie.js └── log4j.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .idea 3 | build 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The what's and when's 2 | 3 | ### What is `akka-http-session` 4 | It is a set of `akka-http` directives, that are used when building Akka HTTP Routes. 5 | They intercept an HTTP request to search for a particular Cookie or Header to extract data, a.k.a. the session. 6 | 7 | ### When do I need `akka-http-session`? 8 | The directives are useful in web applications to send a particular type of data to clients. 9 | They prevent the request and response bodies to be mixed with session data, happening with every HTTP call. 10 | 11 | ### What typical use cases does `akka-http-session` solve? 12 | One goal is to login with a stateless protocol (HTTP) and retrieve data through subsequent calls without re-authenticating each one. 13 | 14 | The client logs in and receives a token. 15 | This token is used by the client to make further requests. 16 | It is the client's responsibility to invalidate this token, once it is no longer needed. 17 | Typically this is the case, when a user logs out. 18 | 19 | An additional feature is the possibility to send data as part of a session. 20 | To prevent this data to be forged, it is signed by the server. 21 | The signature is appended to the data what prevents the client, or an attacker, to modify any piece of it. 22 | 23 | ### What are the limits? 24 | A session should be kept small and simple. 25 | `akka-http-session` does not allow for nested objects inside sessions. 26 | Another hard limit is the size of a session: for Cookies it is restricted to 4kB minus 50B for the server signature appended to the session's data. 27 | 28 | ## Session Data 29 | ### What type of data can be sent in a session? 30 | As long as you provide a serializer for your custom types, you can use them. 31 | Standard types like `String`, `Integer`, `Long`, `Double` and `Map` are supported out of the box. 32 | 33 | [Here's an example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/data_types/StringTypeSession.java) with a `String` data type session: 34 | ``` 35 | $ curl -i --data "a string type" http://localhost:8080/api/do_login 36 | 37 | HTTP/1.1 200 OK 38 | Set-Authorization: 97674A1F8902A2C6FF18D62D4782508428BE1FD2-1505457571316-xa+string+type 39 | Server: akka-http/10.0.9 40 | Date: Fri, 15 Sep 2017 06:34:31 GMT 41 | Content-Type: text/plain; charset=UTF-8 42 | Content-Length: 2 43 | 44 | ok 45 | ``` 46 | [Here's an example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/data_types/LongTypeSession.java) with a `Long` data type session: 47 | ``` 48 | $ curl -i --data "12321" http://localhost:8080/api/do_login 49 | 50 | HTTP/1.1 200 OK 51 | Set-Authorization: E387BECF0D28B42ADF67762AAFE31FA554511D48-1505458157589-x12321 52 | Server: akka-http/10.0.9 53 | Date: Fri, 15 Sep 2017 06:44:17 GMT 54 | Content-Type: text/plain; charset=UTF-8 55 | Content-Length: 2 56 | 57 | ok 58 | ``` 59 | [Here's an example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/data_types/MapTypeSession.java) with a `Map` data type session: 60 | ``` 61 | $ curl -i --data "key1,value1:k2,v2" http://localhost:8080/api/do_login 62 | 63 | HTTP/1.1 200 OK 64 | Set-Authorization: D69B68453648EA25DB6A49F459DC7112D766B5B6-1505987538087-xkey1%3Dvalue1%26k2%3Dv2 65 | Server: akka-http/10.0.9 66 | Date: Thu, 21 Sep 2017 09:47:18 GMT 67 | Content-Type: text/plain; charset=UTF-8 68 | Content-Length: 2 69 | 70 | ok 71 | 72 | $ curl -i -H "Authorization: D69B68453648EA25DB6A49F459DC7112D766B5B6-1505987538087-xkey1%3Dvalue1%26k2%3Dv2" http://localhost:8080/api/current_login 73 | 74 | HTTP/1.1 200 OK 75 | Server: akka-http/10.0.9 76 | Date: Thu, 21 Sep 2017 09:47:44 GMT 77 | Content-Type: text/plain; charset=UTF-8 78 | Content-Length: 6 79 | 80 | value1 81 | ``` 82 | 83 | [Here's an example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/data_types/CustomTypeSession.java) with a `CustomType` data type session: 84 | ``` 85 | $ curl -i --data "my_login,42" http://localhost:8080/api/do_login 86 | 87 | HTTP/1.1 200 OK 88 | Set-Authorization: 1D09A45EDCF4E4060EB88379EB27ABF619FA7C97-1505467994695-xmy_login%2C42 89 | Server: akka-http/10.0.9 90 | Date: Fri, 15 Sep 2017 09:28:14 GMT 91 | Content-Type: text/plain; charset=UTF-8 92 | Content-Length: 2 93 | 94 | ok 95 | ``` 96 | 97 | ## Session Transport 98 | ### How can I transport the session between server and client? 99 | Two transport types are available: Cookies and Headers. 100 | 101 | ### Why would I use Cookies? 102 | Using Cookies to send session data has the advantage that it is handled automatically by client applications, like a web browser. 103 | Also Cookies do not require you to implement a storage, since it's built-in into the browser already. 104 | 105 | The [Cookie transport example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/transport/CookieTransport.java) shows a typical setup for Cookies. Below is a sample use case: 106 | 107 | ``` 108 | $ curl -i http://localhost:8080/api/current_login 109 | 110 | HTTP/1.1 403 Forbidden 111 | Server: akka-http/10.0.9 112 | Date: Thu, 14 Sep 2017 07:33:10 GMT 113 | Content-Type: text/plain; charset=UTF-8 114 | Content-Length: 69 115 | 116 | The supplied authentication is not authorized to access this resource 117 | ``` 118 | As expected, the secured resource is not available. We need to log in first. 119 | ``` 120 | $ curl -i --data "my_login" http://localhost:8080/api/do_login 121 | 122 | HTTP/1.1 200 OK 123 | Set-Cookie: _sessiondata=625617AD3A82A95149B2DAAA6B4444F633F298E5-1505374699373-xmy_login; Path=/; HttpOnly 124 | Server: akka-http/10.0.9 125 | Date: Thu, 14 Sep 2017 07:33:19 GMT 126 | Content-Type: text/plain; charset=UTF-8 127 | Content-Length: 2 128 | 129 | ok 130 | ``` 131 | The response tells us to set the `_sessiondata` Cookie. 132 | ``` 133 | $ curl -i --cookie "_sessiondata=625617AD3A82A95149B2DAAA6B4444F633F298E5-1505374699373-xmy_login" http://localhost:8080/api/current_login 134 | 135 | HTTP/1.1 200 OK 136 | Server: akka-http/10.0.9 137 | Date: Thu, 14 Sep 2017 07:34:13 GMT 138 | Content-Type: text/plain; charset=UTF-8 139 | Content-Length: 8 140 | 141 | my_login 142 | ``` 143 | ### Why would I use Headers? 144 | 145 | Headers are usually used in a non-Cookie world, like mobile. 146 | You need to come up with a storage for the session data on your client application by yourself. 147 | This is what browsers do for you when dealing with Cookies. 148 | Additionally when using refresh tokens, they need to be persisted in that storage as well. 149 | 150 | Take a look at [the Header transport example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/transport/HeaderTransport.java) and how it works: 151 | 152 | ``` 153 | $ curl -i http://localhost:8080/api/current_login 154 | 155 | HTTP/1.1 403 Forbidden 156 | Server: akka-http/10.0.9 157 | Date: Thu, 14 Sep 2017 06:34:49 GMT 158 | Content-Type: text/plain; charset=UTF-8 159 | Content-Length: 69 160 | 161 | The supplied authentication is not authorized to access this resource 162 | ``` 163 | As expected, the secured resource is not available. Let's login first and get an authorization header: 164 | 165 | ``` 166 | $ curl -i --data "my_login" http://localhost:8080/api/do_login 167 | 168 | HTTP/1.1 200 OK 169 | Set-Authorization: 66B4954117229C7E98710B84384DF0ED075B0AC7-1505371227619-xmy_login 170 | Server: akka-http/10.0.9 171 | Date: Thu, 14 Sep 2017 06:35:27 GMT 172 | Content-Type: text/plain; charset=UTF-8 173 | Content-Length: 2 174 | 175 | ok 176 | ``` 177 | Now, knowing what value we should set the Authorization header, we can access the secured resource: 178 | ``` 179 | $ curl -i -H "Authorization: 66B4954117229C7E98710B84384DF0ED075B0AC7-1505371227619-xmy_login" http://localhost:8080/api/current_login 180 | 181 | HTTP/1.1 200 OK 182 | Server: akka-http/10.0.9 183 | Date: Thu, 14 Sep 2017 06:35:57 GMT 184 | Content-Type: text/plain; charset=UTF-8 185 | Content-Length: 8 186 | 187 | my_login 188 | ``` 189 | 190 | ### How long does a session live? 191 | By default a session expires after 1 week, configurable via the `akka.http.session.max-age` config property. 192 | [In this example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/resources/application.conf) it is set to 5 minutes. 193 | When running the [HeaderTransport example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/transport/HeaderTransport.java) we get: 194 | ``` 195 | $ curl -i --data "my_login" http://localhost:8080/api/do_login 196 | 197 | HTTP/1.1 200 OK 198 | Set-Authorization: A9A55BB36713C9CAF020522D88D2FB60F070C5FE-1505470200180-xmy_login 199 | Server: akka-http/10.0.9 200 | Date: Fri, 15 Sep 2017 10:05:00 GMT 201 | Content-Type: text/plain; charset=UTF-8 202 | Content-Length: 2 203 | 204 | ok 205 | ``` 206 | The timestamp attached to the session is `1505470200180` and translates to `Fri Sep 15 2017 10:10:00` which is 5 minutes ahead of the time, when the request was sent `Fri, 15 Sep 2017 10:05:00 GMT`. 207 | 208 | ## Session Continuity 209 | ### What type of sessions are available 210 | There is the `OneOff` session which when expired is of no use anymore. 211 | An alternative is the `Refreshable` session. 212 | It will provide the client with an additional `_refreshtoken` Cookie or `Set-Refresh-Token` Header, dependent on the transport type. 213 | The [RefreshableSession](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/continuity/RefreshableSession.java) example shows how to use this type of sessions: 214 | 215 | ``` 216 | $ curl -i --data "my_login_" http://localhost:8080/api/do_login 217 | 218 | HTTP/1.1 200 OK 219 | Set-Authorization: 018BB450A1F98C3B6A2EFE147A72F5625B2B6196-1505914981440-xmy_login_ 220 | Set-Refresh-Token: h015ti5a3mpdi15g:obv960hi3qic2km08bdnmuq4g6auo9elnhga7igs4p5inm0ep5miaih4sbfkijnq 221 | Server: akka-http/10.0.9 222 | Date: Wed, 20 Sep 2017 13:38:01 GMT 223 | Content-Type: text/plain; charset=UTF-8 224 | Content-Length: 2 225 | 226 | ok 227 | ``` 228 | This session will expire on `1505914981440` - `Wed Sep 20 2017 13:43:01`: 229 | ``` 230 | $ curl -i -H "Authorization: 018BB450A1F98C3B6A2EFE147A72F5625B2B6196-1505914981440-xmy_login_" http://localhost:8080/api/current_login 231 | 232 | HTTP/1.1 200 OK 233 | Server: akka-http/10.0.9 234 | Date: Wed, 20 Sep 2017 13:42:38 GMT 235 | Content-Type: text/plain; charset=UTF-8 236 | Content-Length: 9 237 | 238 | my_login_ 239 | 240 | $ curl -i -H "Authorization: 018BB450A1F98C3B6A2EFE147A72F5625B2B6196-1505914981440-xmy_login_" http://localhost:8080/api/current_login 241 | 242 | HTTP/1.1 403 Forbidden 243 | Server: akka-http/10.0.9 244 | Date: Wed, 20 Sep 2017 13:45:09 GMT 245 | Content-Type: text/plain; charset=UTF-8 246 | Content-Length: 69 247 | 248 | The supplied authentication is not authorized to access this resource 249 | ``` 250 | Now we can use the refresh token to renew the session and receive a new refresh token: 251 | ``` 252 | $ curl -i -H "Refresh-Token: h015ti5a3mpdi15g:obv960hi3qic2km08bdnmuq4g6auo9elnhga7igs4p5inm0ep5miaih4sbfkijnq" http://localhost:8080/api/current_login 253 | 254 | HTTP/1.1 200 OK 255 | Set-Authorization: E857CEB406BE7F7C695B0E0822417100C2F6D252-1505915599565-xmy_login_ 256 | Set-Refresh-Token: oi74uleiqqcutb7j:r5ebs27dbb3q89h6g2bvcpma9sltpad482tebah8doq7nlh48525rgu8511gbplv 257 | Server: akka-http/10.0.9 258 | Date: Wed, 20 Sep 2017 13:48:19 GMT 259 | Content-Type: text/plain; charset=UTF-8 260 | Content-Length: 9 261 | 262 | my_login_ 263 | ``` 264 | 265 | ### When do I need refreshable sessions? 266 | A refreshable session is typically used for "remember-me" functionality. 267 | This is especially useful in mobile applications, where you log in once, and the session is remembered for a long time. 268 | Make sure to adjust the `akka.http.session.refresh-token.max-age` config property in [application.conf](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/resources/application.conf) which defaults to 1 month. 269 | Also, these sessions are persisted. 270 | Although the default implementation stores the refresh tokens in-memory and they won't survive a server restart, 271 | they still can be thought of a way of storing session details on the server side. 272 | 273 | ### Can I use an expired session? 274 | No. Once a session is expired, it cannot be used. 275 | The refresh token instructs the server to create a new session. 276 | Also, when sending a refresh token to renew a session, a new refresh token is issued as well. 277 | Once a refresh token has been submitted to the server it cannot be used a second time: 278 | ``` 279 | $ curl -i -H "Refresh-Token: oi74uleiqqcutb7j:r5ebs27dbb3q89h6g2bvcpma9sltpad482tebah8doq7nlh48525rgu8511gbplv" http://localhost:8080/api/current_login 280 | 281 | HTTP/1.1 200 OK 282 | Set-Authorization: 898CFBD951ED19EFE2BBD65DF56436BD5678E75E-1505915676406-xmy_login_ 283 | Set-Refresh-Token: qfqipvl8i7ig1lpl:6bacamecnkin5m6f3hd0nd03qh0089isvl9cbb9ra6crgh0lc54sde2p6rrk2p64 284 | Server: akka-http/10.0.9 285 | Date: Wed, 20 Sep 2017 13:49:36 GMT 286 | Content-Type: text/plain; charset=UTF-8 287 | Content-Length: 9 288 | 289 | my_login_ 290 | 291 | $ curl -i -H "Refresh-Token: oi74uleiqqcutb7j:r5ebs27dbb3q89h6g2bvcpma9sltpad482tebah8doq7nlh48525rgu8511gbplv" http://localhost:8080/api/current_login 292 | 293 | HTTP/1.1 403 Forbidden 294 | Server: akka-http/10.0.9 295 | Date: Wed, 20 Sep 2017 13:49:39 GMT 296 | Content-Type: text/plain; charset=UTF-8 297 | Content-Length: 69 298 | 299 | The supplied authentication is not authorized to access this resource 300 | ``` 301 | 302 | ### Is the additional refresh token persistent? 303 | Yes. Therefore using refreshable sessions requires you to implement a storage for these tokens. 304 | An in-memory storage is provided as shown in the [RefreshableSession](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/continuity/RefreshableSession.java) example. 305 | However using an in-memory database will invalidate all your refresh tokens when the server restarts. 306 | 307 | In this example a refresh token is issued and before the second request is sent, the server is restarted: 308 | ``` 309 | $ curl -i --data "my_login_" http://localhost:8080/api/do_login 310 | 311 | HTTP/1.1 200 OK 312 | Set-Authorization: E4D6AECD0E39B869949155868CB3E75756606AFE-1505916579385-xmy_login_ 313 | Set-Refresh-Token: lhl4r4rpf53idp3m:8b5tpiohsa58bcm1ajmiev4ja0p54e6qvqdcgbinqmp81agm95rlileo6s8ptiap 314 | Server: akka-http/10.0.9 315 | Date: Wed, 20 Sep 2017 14:04:39 GMT 316 | Content-Type: text/plain; charset=UTF-8 317 | Content-Length: 2 318 | 319 | ok 320 | 321 | $ curl -i -H "Refresh-Token: lhl4r4rpf53idp3m:8b5tpiohsa58bcm1ajmiev4ja0p54e6qvqdcgbinqmp81agm95rlileo6s8ptiap" http://localhost:8080/api/current_login 322 | 323 | HTTP/1.1 403 Forbidden 324 | Server: akka-http/10.0.9 325 | Date: Wed, 20 Sep 2017 14:05:04 GMT 326 | Content-Type: text/plain; charset=UTF-8 327 | Content-Length: 69 328 | 329 | The supplied authentication is not authorized to access this resource 330 | ``` 331 | The refresh token is useless. The server is not able to find the refresh token in the storage: 332 | ``` 333 | 2017-09-20 14:05:04 INFO RefreshableSession:47 - Looking up token for selector: lhl4r4rpf53idp3m, found: false 334 | ``` 335 | 336 | ### How do I enable refreshable sessions? 337 | The `akka-http-session` [directives](#directives) require you to pass a session continuity type. 338 | This can be either `OneOff` or `Refreshable`. 339 | 340 | ## Security 341 | ### Can a Cookie be stolen and be reused by an attacker? 342 | Yes. 343 | 344 | ### How can I use Cookies in a secure way? 345 | 1. Use the `invalidateSession` directive when a user logs out or doesn't need that session any longer 346 | 347 | This is demonstrated by the `do_logout` route in the [CookieTransport example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/transport/CookieTransport.java). 348 | Its purpose is to send an empty `_sessiondata` Cookie to the client (typically a browser). 349 | In consequence, the browser should erase that Cookie to prevent an attacker to read the cookie later on. 350 | ``` 351 | $ curl -i -X POST --cookie "_sessiondata=625617AD3A82A95149B2DAAA6B4444F633F298E5-1505374699373-xmy_login" http://localhost:8080/api/do_logout 352 | 353 | HTTP/1.1 200 OK 354 | Set-Cookie: _sessiondata=deleted; Expires=Wed, 01 Jan 1800 00:00:00 GMT; Path=/; HttpOnly 355 | Server: akka-http/10.0.9 356 | Date: Thu, 14 Sep 2017 07:34:46 GMT 357 | Content-Type: text/plain; charset=UTF-8 358 | Content-Length: 2 359 | 360 | ok 361 | ``` 362 | The Cookie does no longer contain any session data. 363 | It's still possible, that although the browser erased the Cookie, the attacker got it. Therefore: 364 | 365 | 2. Use a sensible `max-age` value which defaults to 7 days 366 | 367 | For use cases where it make sense, set the `max-age` property to a low value, for example `5 minutes`. 368 | This is especially true, when your application allows to access sensitive data, like bank accounts, emails, etc. 369 | The `max-age` property is set in `application.conf`, like in [this example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/resources/application.conf). 370 | Using a cookie which expired on `Thu Sep 14 2017 07:38:19` results in a `403` response, when used in a request at `07:39:43`: 371 | ``` 372 | $ curl -i --cookie "_sessiondata=625617AD3A82A95149B2DAAA6B4444F633F298E5-1505374699373-xmy_login" http://localhost:8080/api/current_login 373 | 374 | HTTP/1.1 403 Forbidden 375 | Server: akka-http/10.0.9 376 | Date: Thu, 14 Sep 2017 07:39:43 GMT 377 | Content-Type: text/plain; charset=UTF-8 378 | Content-Length: 69 379 | 380 | The supplied authentication is not authorized to access this resource 381 | ``` 382 | 3. Secure your transfer protocol - use HTTPS. 383 | 384 | 4. Enable the `Secure` option for Cookies 385 | 386 | The `Secure` attribute is explained in [RFC 6265](https://tools.ietf.org/html/rfc6265#section-4.1.2.5) in more detail. 387 | This does not prevent the server from sending the Cookie to the client. 388 | It's just a flag for the client. 389 | It prevents the browser from sending Cookies, if the request is not transmitted over HTTPS. 390 | There may be 3 types of Cookies being used in `akka-http-session` and all need to have this option enabled explicitly: 391 | `akka.http.session.cookie.secure`, `csrf.cookie.secure` and `refresh-token.cookie.secure`. 392 | 393 | ### Even when a session is invalidated by `invalidateSession`, is it still available? 394 | Yes. 395 | The invalidation is just a way to inform the client, that it should erase the cookie. 396 | There is no session maintenance going on on the server side. 397 | This means, a Cookie representing an invalidated session can still be used: 398 | ``` 399 | $ curl -i --cookie "_sessiondata=625617AD3A82A95149B2DAAA6B4444F633F298E5-1505374699373-xmy_login" http://localhost:8080/api/current_login 400 | 401 | HTTP/1.1 200 OK 402 | Server: akka-http/10.0.9 403 | Date: Thu, 14 Sep 2017 07:34:55 GMT 404 | Content-Type: text/plain; charset=UTF-8 405 | Content-Length: 8 406 | 407 | my_login 408 | ``` 409 | 410 | ### What happens if the Header / Cookie expires 411 | 412 | The authorization Header / Cookie contains a timestamp. 413 | In the example below it is `1505371227619`, which translates to `Thu Sep 14 2017 06:40:27`. 414 | ``` 415 | $ curl -i -H "Authorization: 66B4954117229C7E98710B84384DF0ED075B0AC7-1505371227619-xmy_login" http://localhost:8080/api/current_login 416 | HTTP/1.1 200 OK 417 | Server: akka-http/10.0.9 418 | Date: Thu, 14 Sep 2017 06:39:27 GMT 419 | Content-Type: text/plain; charset=UTF-8 420 | Content-Length: 8 421 | 422 | my_login 423 | ``` 424 | As expected this one passed, since the authorization Header has not expired yet. 425 | The request below is from `Thu, 14 Sep 2017 06:43:01` and therefore should fail to access the resource: 426 | ``` 427 | $ curl -i -H "Authorization: 66B4954117229C7E98710B84384DF0ED075B0AC7-1505371227619-xmy_login" http://localhost:8080/api/current_login 428 | HTTP/1.1 403 Forbidden 429 | Server: akka-http/10.0.9 430 | Date: Thu, 14 Sep 2017 06:43:01 GMT 431 | Content-Type: text/plain; charset=UTF-8 432 | Content-Length: 69 433 | 434 | The supplied authentication is not authorized to access this resource 435 | ``` 436 | 437 | ### Is it possible to renew a timed out authorization Header / Cookie? 438 | When using `oneOffSessions`, just setting the timestamp to a future date won't have any effect. 439 | It is secured by the server by signing the Header / Cookie data. 440 | Here we set the timestamp to `Mon Sep 25 2017 20:27:07` and executed the request on `Thu, 14 Sep 2017 06:43:10`: 441 | ``` 442 | $ curl -i -H "Authorization: 66B4954117229C7E98710B84384DF0ED075B0AC7-1506371227619-xmy_login" http://localhost:8080/api/current_login 443 | HTTP/1.1 403 Forbidden 444 | Server: akka-http/10.0.9 445 | Date: Thu, 14 Sep 2017 06:43:10 GMT 446 | Content-Type: text/plain; charset=UTF-8 447 | Content-Length: 69 448 | 449 | The supplied authentication is not authorized to access this resource 450 | ``` 451 | To renew sessions, use the [Refreshable](#refreshable) transport. 452 | 453 | ### Is it reasonable to expire sessions after 1 week? 454 | It depends ;) 455 | A cookie may be stolen from a browser, when someone forgets to logout. 456 | In this case a more sensitive value may make sense. 457 | On the other hand, a mobile application may play well with a longer expiry value, provided the phone itself is secured from unauthorised access. 458 | In such a use-case however, [Refresh Tokens](#refreshable) which are valid for 1 week by default, may make more sense. 459 | 460 | ### Is it possible to modify the session data on the client side? 461 | No. The session is signed by the server and changes to the session's content are picked up and rejected. 462 | In other words, it is not possible to login with valid credentials and then having a valid token pretend to be a different user, like in this example: 463 | 464 | ``` 465 | $ curl -i -H "Authorization: EAA15F51D825EFBCC1A2A0D43C65CFCA505F2497-1505383157632-xmy_login" http://localhost:8080/api/current_login 466 | 467 | HTTP/1.1 200 OK 468 | Server: akka-http/10.0.9 469 | Date: Thu, 14 Sep 2017 09:54:37 GMT 470 | Content-Type: text/plain; charset=UTF-8 471 | Content-Length: 8 472 | 473 | my_login 474 | ``` 475 | Trying to provide the server with a modified session (`my_login` is set to `a_different_login`) results in an error: 476 | 477 | ``` 478 | $ curl -i -H "Authorization: EAA15F51D825EFBCC1A2A0D43C65CFCA505F2497-1505383157632-xa_different_login" http://localhost:8080/api/current_login 479 | 480 | HTTP/1.1 403 Forbidden 481 | Server: akka-http/10.0.9 482 | Date: Thu, 14 Sep 2017 09:54:41 GMT 483 | Content-Type: text/plain; charset=UTF-8 484 | Content-Length: 69 485 | 486 | The supplied authentication is not authorized to access this resource 487 | ``` 488 | 489 | ### What does encryption provide me with? 490 | Enabling session data encryption allows to send data in a format that is not readable by the client. 491 | To enable session data encryption set the `akka.http.session.encrypt-data` config property in `application.conf`, like in [this resource file](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/resources/application.conf). 492 | As seen below, the client received a token, that can be used to access the secured resource, but the data itself is encrypted. 493 | 494 | ``` 495 | $ curl -i --data "my_login" http://localhost:8080/api/do_login 496 | 497 | HTTP/1.1 200 OK 498 | Set-Authorization: 954814B09DA2583BB7AD9E37DFB69436E44111D8-EFDA5F903AA65EFFF7AC7C0DE31A7909A639C54E5332A0C24DDDE09D9B4384A4 499 | Server: akka-http/10.0.9 500 | Date: Thu, 14 Sep 2017 10:04:13 GMT 501 | Content-Type: text/plain; charset=UTF-8 502 | Content-Length: 2 503 | 504 | ok 505 | ``` 506 | 507 | ``` 508 | $ curl -i -H "Authorization: 954814B09DA2583BB7AD9E37DFB69436E44111D8-EFDA5F903AA65EFFF7AC7C0DE31A7909A639C54E5332A0C24DDDE09D9B4384A4" http://localhost:8080/api/current_login 509 | 510 | HTTP/1.1 200 OK 511 | Server: akka-http/10.0.9 512 | Date: Thu, 14 Sep 2017 10:06:09 GMT 513 | Content-Type: text/plain; charset=UTF-8 514 | Content-Length: 8 515 | 516 | my_login 517 | ``` 518 | 519 | ### Why would I encrypt session data? 520 | Encryption is used to send data and receive it from the client for further processing but prevent the client from seeing it. 521 | Typically the session contains a user id. 522 | You may not want to share it for various reasons, like when it is a sequential number revealing how many customers you have at least. 523 | Another example would be data that is expensive to fetch or require access to a paid API or a 3rd party call. 524 | If such data has to be put in context with a particular user, therefore sent as part of the session data, and the client should not be able to read it, then encryption is the way to go. 525 | 526 | ## Session Directives 527 | ### What are these session directives exactly for? 528 | Including `akka-http-session` directives into the route chain, you can require an endpoint to be accessible only, if a valid session is provided by the client. 529 | Also there's a directive to initiate or invalidate a session when accessing a particular endpoint. 530 | This is especially useful when logging in or out. 531 | 532 | ### What is the `setSession` directive good for? 533 | Adding this directive to a route chain allows you to initialize a session. 534 | Depending on the transport type, either a `Set-Authorization` header or a `Set-Cookie` header are set with a new session. 535 | It is up to the client to read the appropriate Header and use the session in subsequent calls. 536 | 537 | ### What is the `session` directive good for? 538 | This directive is responsible for extracting the session result to be used further on the server side. 539 | A session result can be `Decoded` (valid), `CreatedFromToken`, `Expired`, `Corrupt` or having no token present `TokenNotFound` 540 | The directive does not require the client to provide a session. 541 | However if a session is present, a result is made available to the server for further processing. 542 | [This example](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/directives/SessionDirective.java) shows two possibilities, either there is no session at all, or the session is present. 543 | ``` 544 | $ curl -i --data "session_details" http://localhost:8080/api/do_login 545 | 546 | HTTP/1.1 200 OK 547 | Set-Authorization: D3A3AC31E69D96A41DA482198B367364AEFDFEA1-1505735526813-xsession_details 548 | Server: akka-http/10.0.9 549 | Date: Mon, 18 Sep 2017 11:47:06 GMT 550 | Content-Type: text/plain; charset=UTF-8 551 | Content-Length: 2 552 | 553 | ok 554 | ``` 555 | A request to the `do_login` endpoint replies with a session. 556 | If we decide not to use it, the server replies with `no session`, according to the source code. 557 | ``` 558 | $ curl -i http://localhost:8080/api/current_login 559 | 560 | HTTP/1.1 200 OK 561 | Server: akka-http/10.0.9 562 | Date: Mon, 18 Sep 2017 11:47:17 GMT 563 | Content-Type: text/plain; charset=UTF-8 564 | Content-Length: 10 565 | 566 | no session 567 | ``` 568 | If however the client decides to provide session details, the reply contains them: 569 | ``` 570 | $ curl -i -H "Authorization: D3A3AC31E69D96A41DA482198B367364AEFDFEA1-1505735526813-xsession_details" http://localhost:8080/api/current_login 571 | 572 | HTTP/1.1 200 OK 573 | Server: akka-http/10.0.9 574 | Date: Mon, 18 Sep 2017 11:47:33 GMT 575 | Content-Type: text/plain; charset=UTF-8 576 | Content-Length: 15 577 | 578 | session_details 579 | ``` 580 | A more detailed example is available in [VariousSessions.java](https://github.com/softwaremill/akka-http-session/tree/master/example/src/main/java/com/softwaremill/example/session/VariousSessionsJava.java). 581 | 582 | ### What is the `invalidateSession` directive good for? 583 | This directive instructs the client to clean the Cookie or Header. 584 | In browsers, this is done automatically, but in your custom client application, you need to take care for that by your self. 585 | Take a look at [How can I use Cookies in a secure way?](#secure-cookie) and [Even when a session is invalidated by `invalidateSession`, is it still available?](#invalidate-session) for some important details on this directive and examples on how to use it. 586 | 587 | ### What is the `optionalSession` directive good for? 588 | This one is very similar to the `session` directive. 589 | In this case however, we get access to the session details, which is an `Optional`. 590 | Based on that we can decide on the server side, how to proceed. 591 | In the [OptionalSession](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/directives/OptionalSessionDirective.java) example, we either reply with `no session` or with the session details, if present. 592 | 593 | ### What is the `requiredSession` directive good for? 594 | This directive is used to secure endpoints. 595 | Submitting a HTTP request requires the client to provide a valid session. 596 | A session can be requested through an endpoint configured with the `setSession` directive. 597 | The [CookieTransport](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/transport/CookieTransport.java) and [HeaderTransport](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/transport/HeaderTransport.java) examples show how to use this directive. 598 | Also a sample use case of securing an endpoint is shown in [the Cookie](#cookies) and [the Header](#headers) transport example. 599 | 600 | ### What is the `touchRequiredSession` directive good for? 601 | Sessions do expire and the max age is configurable, as mentioned in [How long does a session live?](#max-age). 602 | If you want to expose an endpoint that will reset the expiry date, include the `touchRequiredSession` in the route chain. 603 | Besides the expiry date, the whole token will change. 604 | Again when using Cookies, the browser will handle this, and replace the old Cookie, but in your own client application you have to replace the Cookie or Header by yourself. 605 | This directive is demonstrated in the [TouchRequiredSession](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/directives/TouchRequiredSessionDirective.java) example: 606 | ``` 607 | $ curl -i --data "my_login" http://localhost:8080/api/do_login 608 | 609 | HTTP/1.1 200 OK 610 | Set-Authorization: 170FBA501897D8F338C9680C8198F39B02C6C852-1505741661309-xmy_login 611 | Server: akka-http/10.0.9 612 | Date: Mon, 18 Sep 2017 13:29:21 GMT 613 | Content-Type: text/plain; charset=UTF-8 614 | Content-Length: 2 615 | 616 | ok 617 | ``` 618 | The expiry date of the session is set to `1505741661309`, which is `Mon Sep 18 2017 13:34:21`. 619 | The session life time is set to 5 minutes, as described earlier. 620 | We can now access a resource, that requires a valid session, as usual: 621 | ``` 622 | $ curl -i -H "Authorization: 170FBA501897D8F338C9680C8198F39B02C6C852-1505741661309-xmy_login" http://localhost:8080/api/current_login 623 | HTTP/1.1 200 OK 624 | Server: akka-http/10.0.9 625 | Date: Mon, 18 Sep 2017 13:29:45 GMT 626 | Content-Type: text/plain; charset=UTF-8 627 | Content-Length: 8 628 | 629 | my_login 630 | ``` 631 | Touching the session extends the expiry date by another 5 minutes: 632 | ``` 633 | $ curl -i -X POST -H "Authorization: 170FBA501897D8F338C9680C8198F39B02C6C852-1505741661309-xmy_login" http://localhost:8080/api/touch 634 | 635 | HTTP/1.1 200 OK 636 | Set-Authorization: 5B28616D1DF413964CF8D76CD6BB249CBFFB52B6-1505741866892-xmy_login 637 | Server: akka-http/10.0.9 638 | Date: Mon, 18 Sep 2017 13:32:46 GMT 639 | Content-Type: text/plain; charset=UTF-8 640 | Content-Length: 15 641 | 642 | session touched 643 | ``` 644 | Now the expiry date is `1505741866892`, which is set to `Mon Sep 18 2017 13:37:46`. 645 | That's 5 minutes later than the request was issued (`Mon, 18 Sep 2017 13:32:46`). 646 | 647 | ### What happens if a timed out or invalid session is touched? 648 | The `touchRequiredSession` requires a valid session to be extended. 649 | If an expired session is passed, the server will reply with an error. 650 | ``` 651 | $ curl -i -X POST -H "Authorization: 170FBA501897D8F338C9680C8198F39B02C6C852-1505741661309-xmy_login" http://localhost:8080/api/touch 652 | 653 | HTTP/1.1 403 Forbidden 654 | Server: akka-http/10.0.9 655 | Date: Mon, 18 Sep 2017 13:38:11 GMT 656 | Content-Type: text/plain; charset=UTF-8 657 | Content-Length: 69 658 | 659 | The supplied authentication is not authorized to access this resource 660 | ``` 661 | In this case the session expired on `1505741661309`, which is `Mon Sep 18 2017 13:34:21`, and the touch request was issued on `Mon, 18 Sep 2017 13:38:11`. 662 | 663 | ## JWT 664 | ### How do I use Json Web Tokens 665 | In case you want to use the JWT format for authorization tokens, replace the `BasicSessionEncoder` with the `JwtSessionEncoder` and choose one of the JWT serializers, like `JwtSessionSerializers.StringToJValueSessionSerializer`. 666 | The [JwtEncodedSession](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/jwt/JwtEncodedSession.java) example shows this particular use case: 667 | ``` 668 | $ curl -i --data "my_login" http://localhost:8080/api/do_login 669 | 670 | HTTP/1.1 200 OK 671 | Set-Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoibXlfbG9naW4iLCJleHAiOjE1MDU4Mjg3NjZ9.XCBN+29g11bwE6k/eDHfw0YETiGCEh38n/5tQuFMU/0= 672 | Server: akka-http/10.0.9 673 | Date: Tue, 19 Sep 2017 13:41:06 GMT 674 | Content-Type: text/plain; charset=UTF-8 675 | Content-Length: 2 676 | 677 | ok 678 | 679 | $ curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoibXlfbG9naW4iLCJleHAiOjE1MDU4Mjg3NjZ9.XCBN+29g11bwE6k/eDHfw0YETiGCEh38n/5tQuFMU/0=" http://localhost:8080/api/current_login 680 | 681 | HTTP/1.1 200 OK 682 | Server: akka-http/10.0.9 683 | Date: Tue, 19 Sep 2017 13:41:46 GMT 684 | Content-Type: text/plain; charset=UTF-8 685 | Content-Length: 8 686 | 687 | my_login 688 | ``` 689 | 690 | ## CSRF protection 691 | ### What is it and (when) do I need it? 692 | A CSRF attack is an attack, which tries to re-use a valid cookie to issue a request on your behalf. 693 | In short, you login to your bank account. A session Cookie is sent back to you. 694 | Now with every request to the bank's site, the Cookie is sent by the browser. 695 | If you open a new tab in your browser and navigate to a malicious web site you may find a prepared link. 696 | Clicking on that link will do a POST request to your bank's site. 697 | Since it is the bank's site, the session Cookie you received from your bank is also sent, hence the request is authorized. 698 | Now the POST request could modify your accounts data as you can do on the bank's site, when logged in. 699 | Typically you won't even find a link to click. 700 | It could be javascript or an `` html tag which does a GET request. 701 | 702 | CSRF protection is only needed when using Cookie type transport. 703 | 704 | The idea behind protecting against CSRF attacks is to use a double-submit cookie. 705 | The server sends an additional XSRF token to the client (browser). 706 | Every request has to include that cookie and additionally an HTTP Header with the same value. 707 | The attacker is able to prepare a link and relies on the browser to send the `_sessiondata` cookie, as well the XSRF token. 708 | But the attacker is not able to read the cookie value. Hence the attacker is not able to set the additional Header to the right value. 709 | 710 | ### How do I enable CSRF protection 711 | Two directives, `randomTokenCsrfProtection` and `setNewCsrfToken`, are required. 712 | The example [CsrfProtection](https://github.com/softwaremill/akka-http-session-faq/tree/master/src/main/java/session/csrf/CsrfProtection.java) uses both directives. 713 | Finally a `CheckHeader` component is required to intercept the route and lookup the header for the CSRF token. 714 | 715 | The first one, `randomTokenCsrfProtection`, sets a new CSRF token on every GET request in form of a `XSRF-TOKEN` Cookie. 716 | ``` 717 | $ curl -i http://localhost:8080/ 718 | 719 | HTTP/1.1 200 OK 720 | Set-Cookie: XSRF-TOKEN=mm10u06r81ltjqf7c62c0pn0pc7opssl7gm2ucckom5e4mp0gjsvhn8pa8vr8ula; Path=/ 721 | Server: akka-http/10.0.9 722 | Date: Fri, 22 Sep 2017 11:48:42 GMT 723 | Content-Type: text/plain; charset=UTF-8 724 | Content-Length: 7 725 | 726 | Welcome 727 | ``` 728 | Without that cookie we would be not able to access the `/api/do_login` endpoint: 729 | ``` 730 | $ curl -i --data "my_login" http://localhost:8080/api/do_login 731 | HTTP/1.1 403 Forbidden 732 | Server: akka-http/10.0.9 733 | Date: Fri, 22 Sep 2017 11:49:01 GMT 734 | Content-Type: text/plain; charset=UTF-8 735 | Content-Length: 69 736 | 737 | The supplied authentication is not authorized to access this resource 738 | ``` 739 | Let's try again with the `XSRF-TOKEN` Cookie and `X-XSRF-TOKEN` Header: 740 | ``` 741 | $ curl -i --data "my_login" --cookie "XSRF-TOKEN=mm10u06r81ltjqf7c62c0pn0pc7opssl7gm2ucckom5e4mp0gjsvhn8pa8vr8ula" -H "X-XSRF-TOKEN: mm10u06r81ltjqf7c62c0pn0pc7opssl7gm2ucckom5e4mp0gjsvhn8pa8vr8ula" http://localhost:8080/api/do_login 742 | 743 | HTTP/1.1 200 OK 744 | Set-Cookie: _sessiondata=5DEF1181A728E6C1724D263B23A8ABAF859046A8-1506081618995-xmy_login; Path=/; HttpOnly 745 | Set-Cookie: XSRF-TOKEN=isael6lds7q5q4ilm90hnv96s0vqn4o7i7pi4mp8b5q27eeq4ug23bto03vu4fmn; Path=/ 746 | Server: akka-http/10.0.9 747 | Date: Fri, 22 Sep 2017 11:50:18 GMT 748 | Content-Type: text/plain; charset=UTF-8 749 | Content-Length: 2 750 | 751 | ok 752 | ``` 753 | Notice we received a new `XSRF-TOKEN` value. 754 | This is achieved by the `setNewCsrfToken` directive. 755 | This is recommended to prevent a [session fixation attack](https://security.stackexchange.com/questions/22903/why-refresh-csrf-token-per-form-request). 756 | Now we can access the `/api/do_logout` endpoint: 757 | ``` 758 | $ curl -i -X POST --cookie "_sessiondata=5DEF1181A728E6C1724D263B23A8ABAF859046A8-1506081618995-xmy_login;XSRF-TOKEN=mm10u06r81ltjqf7c62c0pn0pc7opssl7gm2ucckom5e4mp0gjsvhn8pa8vr8ula" -H "X-XSRF-TOKEN: mm10u06r81ltjqf7c62c0pn0pc7opssl7gm2ucckom5e4mp0gjsvhn8pa8vr8ula" http://localhost:8080/api/do_logout 759 | 760 | HTTP/1.1 200 OK 761 | Set-Cookie: _sessiondata=deleted; Expires=Wed, 01 Jan 1800 00:00:00 GMT; Path=/; HttpOnly 762 | Server: akka-http/10.0.9 763 | Date: Fri, 22 Sep 2017 11:51:43 GMT 764 | Content-Type: text/plain; charset=UTF-8 765 | Content-Length: 2 766 | 767 | ok 768 | ``` 769 | Remember that accessing an endpoint via `GET` is not secured by the CSRF protection mechanism. 770 | Therefore endpoints accessible through `GET` should not mutate the server's state in any way: 771 | ``` 772 | $ curl -i --cookie "_sessiondata=5DEF1181A728E6C1724D263B23A8ABAF859046A8-1506081618995-xmy_login" http://localhost:8080/api/current_login 773 | 774 | HTTP/1.1 200 OK 775 | Set-Cookie: XSRF-TOKEN=4ggc4k5ba4i9ovuu4sb32pm9kdvc8fomarqgt0fjih1s5s5ltnboi8e08efo81cp; Path=/ 776 | Server: akka-http/10.0.9 777 | Date: Fri, 22 Sep 2017 11:55:53 GMT 778 | Content-Type: text/plain; charset=UTF-8 779 | Content-Length: 8 780 | 781 | my_login 782 | ``` -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'akka-http-session-faq' 2 | version '1.0-SNAPSHOT' 3 | 4 | apply plugin: 'java' 5 | 6 | sourceCompatibility = 1.8 7 | 8 | repositories { 9 | mavenLocal() 10 | mavenCentral() 11 | } 12 | 13 | def akkaV = '2.5.19' 14 | def akkaHttpV = '10.1.8' 15 | def akkaHttpSessionV = '0.5.11' 16 | 17 | dependencies { 18 | compile "com.typesafe.akka:akka-actor_2.11:$akkaV" 19 | compile "com.typesafe.akka:akka-stream_2.11:$akkaV" 20 | compile "com.typesafe.akka:akka-http_2.11:$akkaHttpV" 21 | compile "com.softwaremill.akka-http-session:core_2.11:$akkaHttpSessionV" 22 | compile "com.softwaremill.akka-http-session:jwt_2.11:$akkaHttpSessionV" 23 | 24 | compile 'org.slf4j:slf4j-log4j12:1.7.5' 25 | 26 | } 27 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/akka-http-session-faq/cde132dc2cd309656f7fd24bbb066c1b802b79f1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 05 10:26:54 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'faq' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/session/continuity/RefreshableSession.java: -------------------------------------------------------------------------------- 1 | package session.continuity; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.dispatch.MessageDispatcher; 6 | import akka.http.javadsl.ConnectHttp; 7 | import akka.http.javadsl.Http; 8 | import akka.http.javadsl.ServerBinding; 9 | import akka.http.javadsl.model.HttpRequest; 10 | import akka.http.javadsl.model.HttpResponse; 11 | import akka.http.javadsl.server.Route; 12 | import akka.http.javadsl.unmarshalling.Unmarshaller; 13 | import akka.stream.ActorMaterializer; 14 | import akka.stream.javadsl.Flow; 15 | import com.softwaremill.session.BasicSessionEncoder; 16 | import com.softwaremill.session.OneOff; 17 | import com.softwaremill.session.RefreshTokenStorage; 18 | import com.softwaremill.session.Refreshable; 19 | import com.softwaremill.session.SessionConfig; 20 | import com.softwaremill.session.SessionEncoder; 21 | import com.softwaremill.session.SessionManager; 22 | import com.softwaremill.session.SetSessionTransport; 23 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 24 | import com.softwaremill.session.javadsl.InMemoryRefreshTokenStorage; 25 | import com.softwaremill.session.javadsl.SessionSerializers; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | import java.io.IOException; 30 | import java.util.concurrent.CompletionStage; 31 | 32 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 33 | 34 | 35 | public class RefreshableSession extends HttpSessionAwareDirectives { 36 | 37 | private static final Logger LOGGER = LoggerFactory.getLogger(RefreshableSession.class); 38 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 39 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 40 | 41 | // ************************************************************ // 42 | // This is where the refresh token in-memory storage is defined // 43 | // ************************************************************ // 44 | private static final RefreshTokenStorage REFRESH_TOKEN_STORAGE = new InMemoryRefreshTokenStorage() { 45 | @Override 46 | public void log(String msg) { 47 | LOGGER.info(msg); 48 | } 49 | }; 50 | 51 | private Refreshable refreshableSession; 52 | private SetSessionTransport sessionTransport; 53 | 54 | private RefreshableSession(MessageDispatcher dispatcher) { 55 | super(new SessionManager<>( 56 | SessionConfig.defaultConfig(SECRET), 57 | BASIC_ENCODER 58 | ) 59 | ); 60 | // ********************************************************** // 61 | // This is where the Session continuity is set to Refreshable // 62 | // ********************************************************** // 63 | refreshableSession = new Refreshable<>( 64 | getSessionManager(), 65 | REFRESH_TOKEN_STORAGE, 66 | dispatcher 67 | ); 68 | sessionTransport = HeaderST; 69 | } 70 | 71 | public static void main(String[] args) throws IOException { 72 | 73 | // ** akka-http boiler plate ** 74 | ActorSystem system = ActorSystem.create("example"); 75 | final ActorMaterializer materializer = ActorMaterializer.create(system); 76 | final Http http = Http.get(system); 77 | 78 | // ** akka-http-session setup ** 79 | final RefreshableSession app = new RefreshableSession(system.dispatchers().lookup("akka.actor.default-dispatcher")); 80 | 81 | // ** akka-http boiler plate continued ** 82 | final Flow routes = app.createRoutes().flow(system, materializer); 83 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 84 | 85 | System.out.println("Server started, press enter to stop"); 86 | System.in.read(); 87 | 88 | binding 89 | .thenCompose(ServerBinding::unbind) 90 | .thenAccept(unbound -> system.terminate()); 91 | } 92 | 93 | private Route createRoutes() { 94 | return 95 | route( 96 | route( 97 | pathPrefix("api", () -> 98 | route( 99 | path("do_login", () -> 100 | post(() -> 101 | entity(Unmarshaller.entityToString(), body -> { 102 | LOGGER.info("Logging in {}", body); 103 | return setSession(refreshableSession, sessionTransport, body, () -> 104 | extractRequestContext(ctx -> 105 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 106 | complete("ok") 107 | ) 108 | ) 109 | ); 110 | } 111 | ) 112 | ) 113 | ), 114 | 115 | // This should be protected and accessible only when logged in 116 | path("do_logout", () -> 117 | post(() -> 118 | requiredSession(refreshableSession, sessionTransport, session -> 119 | invalidateSession(refreshableSession, sessionTransport, () -> 120 | extractRequestContext(ctx -> { 121 | LOGGER.info("Logging out {}", session); 122 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 123 | complete("ok") 124 | ); 125 | } 126 | ) 127 | ) 128 | ) 129 | ) 130 | ), 131 | 132 | // This should be protected and accessible only when logged in 133 | path("current_login", () -> 134 | get(() -> 135 | requiredSession(refreshableSession, sessionTransport, session -> 136 | extractRequestContext(ctx -> { 137 | LOGGER.info("Current session: " + session); 138 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 139 | complete(session) 140 | ); 141 | } 142 | ) 143 | ) 144 | ) 145 | ) 146 | ) 147 | ) 148 | ) 149 | ); 150 | } 151 | } -------------------------------------------------------------------------------- /src/main/java/session/csrf/CsrfProtection.java: -------------------------------------------------------------------------------- 1 | package session.csrf; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.CheckHeader; 16 | import com.softwaremill.session.OneOff; 17 | import com.softwaremill.session.SessionConfig; 18 | import com.softwaremill.session.SessionEncoder; 19 | import com.softwaremill.session.SessionManager; 20 | import com.softwaremill.session.SetSessionTransport; 21 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 22 | import com.softwaremill.session.javadsl.SessionSerializers; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.io.IOException; 27 | import java.util.concurrent.CompletionStage; 28 | 29 | import static com.softwaremill.session.javadsl.SessionTransports.CookieST; 30 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 31 | 32 | 33 | public class CsrfProtection extends HttpSessionAwareDirectives { 34 | 35 | private static final Logger LOGGER = LoggerFactory.getLogger(CsrfProtection.class); 36 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 37 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 38 | 39 | private OneOff oneOffSession; 40 | private SetSessionTransport sessionTransport; 41 | 42 | private CsrfProtection() { 43 | super(new SessionManager<>( 44 | SessionConfig.defaultConfig(SECRET), 45 | BASIC_ENCODER 46 | ) 47 | ); 48 | oneOffSession = new OneOff<>(getSessionManager()); 49 | sessionTransport = CookieST; 50 | } 51 | 52 | public static void main(String[] args) throws IOException { 53 | 54 | // ** akka-http boiler plate ** 55 | ActorSystem system = ActorSystem.create("example"); 56 | final ActorMaterializer materializer = ActorMaterializer.create(system); 57 | final Http http = Http.get(system); 58 | 59 | // ** akka-http-session setup ** 60 | final CsrfProtection app = new CsrfProtection(); 61 | 62 | // ** akka-http boiler plate continued ** 63 | final Flow routes = app.createRoutes().flow(system, materializer); 64 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 65 | 66 | System.out.println("Server started, press enter to stop"); 67 | System.in.read(); 68 | 69 | binding 70 | .thenCompose(ServerBinding::unbind) 71 | .thenAccept(unbound -> system.terminate()); 72 | } 73 | 74 | private Route createRoutes() { 75 | // ********************************* // 76 | // This is required for CSRF to work // 77 | // ********************************* // 78 | CheckHeader checkHeader = new CheckHeader<>(getSessionManager()); 79 | return 80 | route( 81 | // *************************************************** // 82 | // for each GET request, issue a new random csrf token // 83 | // *************************************************** // 84 | randomTokenCsrfProtection(checkHeader, () -> 85 | route( 86 | pathSingleSlash(() -> 87 | complete("Welcome") 88 | ) 89 | , 90 | route( 91 | pathPrefix("site", () -> 92 | getFromResourceDirectory("")), 93 | 94 | pathPrefix("api", () -> 95 | route( 96 | path("do_login", () -> 97 | post(() -> 98 | entity(Unmarshaller.entityToString(), body -> { 99 | LOGGER.info("Logging in {}", body); 100 | return setSession(oneOffSession, sessionTransport, body, () -> 101 | // ************************************************************ // 102 | // when logged in, issue a new csrf token to prevent a fixation // 103 | // ************************************************************ // 104 | setNewCsrfToken(checkHeader, () -> 105 | extractRequestContext(ctx -> 106 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 107 | complete("ok") 108 | ) 109 | ) 110 | ) 111 | ); 112 | } 113 | ) 114 | ) 115 | ), 116 | 117 | // This should be protected and accessible only when logged in 118 | path("do_logout", () -> 119 | post(() -> 120 | requiredSession(oneOffSession, sessionTransport, session -> 121 | invalidateSession(oneOffSession, sessionTransport, () -> 122 | extractRequestContext(ctx -> { 123 | LOGGER.info("Logging out {}", session); 124 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 125 | complete("ok") 126 | ); 127 | } 128 | ) 129 | ) 130 | ) 131 | ) 132 | ), 133 | 134 | // This should be protected and accessible only when logged in 135 | path("current_login", () -> 136 | get(() -> 137 | requiredSession(oneOffSession, sessionTransport, session -> 138 | extractRequestContext(ctx -> { 139 | LOGGER.info("Current session: " + session); 140 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 141 | complete(session) 142 | ); 143 | } 144 | ) 145 | ) 146 | ) 147 | ) 148 | ) 149 | ) 150 | ) 151 | ) 152 | ) 153 | ); 154 | } 155 | } -------------------------------------------------------------------------------- /src/main/java/session/data_types/CustomType.java: -------------------------------------------------------------------------------- 1 | package session.data_types; 2 | 3 | import com.softwaremill.session.SessionSerializer; 4 | import com.softwaremill.session.SingleValueSessionSerializer; 5 | import com.softwaremill.session.javadsl.SessionSerializers; 6 | import scala.compat.java8.JFunction0; 7 | import scala.compat.java8.JFunction1; 8 | import scala.util.Try; 9 | 10 | public class CustomType { 11 | 12 | /** 13 | * This session serializer converts a session type into a value (always a String type). The first two arguments are just conversion functions. 14 | * The third argument is a serializer responsible for preparing the data to be sent/received over the wire. There are some ready-to-use serializers available 15 | * in the com.softwaremill.session.SessionSerializer companion object, like stringToString and mapToString, just to name a few. 16 | */ 17 | private static final SessionSerializer customTypeSerializer = new SingleValueSessionSerializer<>( 18 | // transform CustomType into String ("myString,myInt") 19 | (JFunction1) (session) -> (session.getMyString().concat(",").concat(session.getMyInt().toString())) 20 | , 21 | // transform String into CustomType 22 | (JFunction1>) (body) -> Try.apply((JFunction0) (() -> 23 | new CustomType(body.split(",")[0], Integer.valueOf(body.split(",")[1])) 24 | )) 25 | , 26 | SessionSerializers.StringToStringSessionSerializer 27 | ); 28 | 29 | private final String myString; 30 | private final Integer myInt; 31 | 32 | public CustomType(String myString, Integer myInt) { 33 | this.myString = myString; 34 | this.myInt = myInt; 35 | } 36 | 37 | public static SessionSerializer getSerializer() { 38 | return customTypeSerializer; 39 | } 40 | 41 | public String getMyString() { 42 | return myString; 43 | } 44 | 45 | public Integer getMyInt() { 46 | return myInt; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/session/data_types/CustomTypeSession.java: -------------------------------------------------------------------------------- 1 | package session.data_types; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import java.io.IOException; 25 | import java.util.concurrent.CompletionStage; 26 | 27 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 28 | 29 | 30 | public class CustomTypeSession extends HttpSessionAwareDirectives { 31 | 32 | private static final Logger LOGGER = LoggerFactory.getLogger(CustomTypeSession.class); 33 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 34 | 35 | // ******************************************************** // 36 | // This is where the Session Data Type is set to CustomType // 37 | // ******************************************************** // 38 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(CustomType.getSerializer()); 39 | 40 | private OneOff oneOffSession; 41 | private SetSessionTransport sessionTransport; 42 | 43 | private CustomTypeSession() { 44 | super(new SessionManager<>( 45 | SessionConfig.defaultConfig(SECRET), 46 | BASIC_ENCODER 47 | ) 48 | ); 49 | oneOffSession = new OneOff<>(getSessionManager()); 50 | sessionTransport = HeaderST; 51 | } 52 | 53 | public static void main(String[] args) throws IOException { 54 | 55 | // ** akka-http boiler plate ** 56 | ActorSystem system = ActorSystem.create("example"); 57 | final ActorMaterializer materializer = ActorMaterializer.create(system); 58 | final Http http = Http.get(system); 59 | 60 | // ** akka-http-session setup ** 61 | final CustomTypeSession app = new CustomTypeSession(); 62 | 63 | // ** akka-http boiler plate continued ** 64 | final Flow routes = app.createRoutes().flow(system, materializer); 65 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 66 | 67 | System.out.println("Server started, press enter to stop"); 68 | System.in.read(); 69 | 70 | binding 71 | .thenCompose(ServerBinding::unbind) 72 | .thenAccept(unbound -> system.terminate()); 73 | } 74 | 75 | private Route createRoutes() { 76 | return 77 | route( 78 | pathPrefix("api", () -> 79 | route( 80 | path("do_login", () -> 81 | post(() -> 82 | entity(Unmarshaller.entityToString(), body -> { 83 | LOGGER.info("Logging in {}", body); 84 | return setSession(oneOffSession, sessionTransport, stringToCustomType(body), () -> 85 | extractRequestContext(ctx -> 86 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 87 | complete("ok") 88 | ) 89 | ) 90 | ); 91 | } 92 | ) 93 | ) 94 | ) 95 | ) 96 | ) 97 | ); 98 | } 99 | 100 | /** 101 | * This helper method converts a String into a CustomType. 102 | * The format of the string is: stringValue,intValue 103 | */ 104 | private CustomType stringToCustomType(String body) { 105 | return new CustomType(body.split(",")[0], Integer.valueOf(body.split(",")[1])); 106 | } 107 | } -------------------------------------------------------------------------------- /src/main/java/session/data_types/LongTypeSession.java: -------------------------------------------------------------------------------- 1 | package session.data_types; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import com.softwaremill.session.javadsl.SessionSerializers; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.util.concurrent.CompletionStage; 27 | 28 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 29 | 30 | 31 | public class LongTypeSession extends HttpSessionAwareDirectives { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(LongTypeSession.class); 34 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 35 | 36 | // ***************************************************** // 37 | // This is where the Session Data Type is set to Long // 38 | // ***************************************************** // 39 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.LongToStringSessionSerializer); 40 | 41 | private OneOff oneOffSession; 42 | private SetSessionTransport sessionTransport; 43 | 44 | private LongTypeSession() { 45 | super(new SessionManager<>( 46 | SessionConfig.defaultConfig(SECRET), 47 | BASIC_ENCODER 48 | ) 49 | ); 50 | oneOffSession = new OneOff<>(getSessionManager()); 51 | sessionTransport = HeaderST; 52 | } 53 | 54 | public static void main(String[] args) throws IOException { 55 | 56 | // ** akka-http boiler plate ** 57 | ActorSystem system = ActorSystem.create("example"); 58 | final ActorMaterializer materializer = ActorMaterializer.create(system); 59 | final Http http = Http.get(system); 60 | 61 | // ** akka-http-session setup ** 62 | final LongTypeSession app = new LongTypeSession(); 63 | 64 | // ** akka-http boiler plate continued ** 65 | final Flow routes = app.createRoutes().flow(system, materializer); 66 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 67 | 68 | System.out.println("Server started, press enter to stop"); 69 | System.in.read(); 70 | 71 | binding 72 | .thenCompose(ServerBinding::unbind) 73 | .thenAccept(unbound -> system.terminate()); 74 | } 75 | 76 | private Route createRoutes() { 77 | return 78 | route( 79 | pathPrefix("api", () -> 80 | route( 81 | path("do_login", () -> 82 | post(() -> 83 | entity(Unmarshaller.entityToString(), body -> { 84 | LOGGER.info("Logging in {}", body); 85 | return setSession(oneOffSession, sessionTransport, Long.valueOf(body), () -> 86 | extractRequestContext(ctx -> 87 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 88 | complete("ok") 89 | ) 90 | ) 91 | ); 92 | } 93 | ) 94 | ) 95 | ) 96 | ) 97 | ) 98 | ); 99 | } 100 | } -------------------------------------------------------------------------------- /src/main/java/session/data_types/MapTypeSession.java: -------------------------------------------------------------------------------- 1 | package session.data_types; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import com.softwaremill.session.javadsl.SessionSerializers; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.util.Map; 27 | import java.util.concurrent.CompletionStage; 28 | 29 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 30 | 31 | 32 | public class MapTypeSession extends HttpSessionAwareDirectives> { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(MapTypeSession.class); 35 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 36 | 37 | // ***************************************************************** // 38 | // This is where the Session Data Type is set to Map // 39 | // ***************************************************************** // 40 | private static final SessionEncoder> BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.MapToStringSessionSerializer); 41 | 42 | private OneOff> oneOffSession; 43 | private SetSessionTransport sessionTransport; 44 | 45 | private MapTypeSession() { 46 | super(new SessionManager<>( 47 | SessionConfig.defaultConfig(SECRET), 48 | BASIC_ENCODER 49 | ) 50 | ); 51 | oneOffSession = new OneOff<>(getSessionManager()); 52 | sessionTransport = HeaderST; 53 | } 54 | 55 | public static void main(String[] args) throws IOException { 56 | 57 | // ** akka-http boiler plate ** 58 | ActorSystem system = ActorSystem.create("example"); 59 | final ActorMaterializer materializer = ActorMaterializer.create(system); 60 | final Http http = Http.get(system); 61 | 62 | // ** akka-http-session setup ** 63 | final MapTypeSession app = new MapTypeSession(); 64 | 65 | // ** akka-http boiler plate continued ** 66 | final Flow routes = app.createRoutes().flow(system, materializer); 67 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 68 | 69 | System.out.println("Server started, press enter to stop"); 70 | System.in.read(); 71 | 72 | binding 73 | .thenCompose(ServerBinding::unbind) 74 | .thenAccept(unbound -> system.terminate()); 75 | } 76 | 77 | private Route createRoutes() { 78 | return 79 | route( 80 | pathPrefix("api", () -> 81 | route( 82 | path("do_login", () -> 83 | post(() -> 84 | entity(Unmarshaller.entityToString(), body -> { 85 | LOGGER.info("Logging in {}", body); 86 | return setSession(oneOffSession, sessionTransport, stringToMap(body), () -> 87 | extractRequestContext(ctx -> 88 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 89 | complete("ok") 90 | ) 91 | ) 92 | ); 93 | } 94 | ) 95 | ) 96 | ), 97 | 98 | // This should be protected and accessible only when logged in 99 | path("current_login", () -> 100 | get(() -> 101 | requiredSession(oneOffSession, sessionTransport, session -> 102 | extractRequestContext(ctx -> { 103 | LOGGER.info("Current session: " + session); 104 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 105 | complete(session.get("key1")) 106 | ); 107 | } 108 | ) 109 | ) 110 | ) 111 | ) 112 | ) 113 | ) 114 | ); 115 | } 116 | 117 | /** 118 | * This helper method converts a session String into a Map. 119 | * The format of the string is: 120 | * key1,value1:key2,value2:key3,value3 121 | */ 122 | private Map stringToMap(String body) { 123 | java.util.Map result = new java.util.HashMap<>(); 124 | for (String tuple : body.split(":")) { 125 | result.put(tuple.split(",")[0], tuple.split(",")[1]); 126 | } 127 | return result; 128 | } 129 | } -------------------------------------------------------------------------------- /src/main/java/session/data_types/StringTypeSession.java: -------------------------------------------------------------------------------- 1 | package session.data_types; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import com.softwaremill.session.javadsl.SessionSerializers; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.util.concurrent.CompletionStage; 27 | 28 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 29 | 30 | 31 | public class StringTypeSession extends HttpSessionAwareDirectives { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(StringTypeSession.class); 34 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 35 | 36 | // ***************************************************** // 37 | // This is where the Session Data Type is set to String // 38 | // ***************************************************** // 39 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 40 | 41 | private OneOff oneOffSession; 42 | private SetSessionTransport sessionTransport; 43 | 44 | private StringTypeSession() { 45 | super(new SessionManager<>( 46 | SessionConfig.defaultConfig(SECRET), 47 | BASIC_ENCODER 48 | ) 49 | ); 50 | oneOffSession = new OneOff<>(getSessionManager()); 51 | sessionTransport = HeaderST; 52 | } 53 | 54 | public static void main(String[] args) throws IOException { 55 | 56 | // ** akka-http boiler plate ** 57 | ActorSystem system = ActorSystem.create("example"); 58 | final ActorMaterializer materializer = ActorMaterializer.create(system); 59 | final Http http = Http.get(system); 60 | 61 | // ** akka-http-session setup ** 62 | final StringTypeSession app = new StringTypeSession(); 63 | 64 | // ** akka-http boiler plate continued ** 65 | final Flow routes = app.createRoutes().flow(system, materializer); 66 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 67 | 68 | System.out.println("Server started, press enter to stop"); 69 | System.in.read(); 70 | 71 | binding 72 | .thenCompose(ServerBinding::unbind) 73 | .thenAccept(unbound -> system.terminate()); 74 | } 75 | 76 | private Route createRoutes() { 77 | return 78 | route( 79 | pathPrefix("api", () -> 80 | route( 81 | path("do_login", () -> 82 | post(() -> 83 | entity(Unmarshaller.entityToString(), body -> { 84 | LOGGER.info("Logging in {}", body); 85 | return setSession(oneOffSession, sessionTransport, body, () -> 86 | extractRequestContext(ctx -> 87 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 88 | complete("ok") 89 | ) 90 | ) 91 | ); 92 | } 93 | ) 94 | ) 95 | ) 96 | ) 97 | ) 98 | ); 99 | } 100 | } -------------------------------------------------------------------------------- /src/main/java/session/directives/OptionalSessionDirective.java: -------------------------------------------------------------------------------- 1 | package session.directives; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import com.softwaremill.session.javadsl.SessionSerializers; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.util.concurrent.CompletionStage; 27 | 28 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 29 | 30 | 31 | public class OptionalSessionDirective extends HttpSessionAwareDirectives { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(OptionalSessionDirective.class); 34 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 35 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 36 | 37 | private OneOff oneOffSession; 38 | private SetSessionTransport sessionTransport; 39 | 40 | private OptionalSessionDirective() { 41 | super(new SessionManager<>( 42 | SessionConfig.defaultConfig(SECRET), 43 | BASIC_ENCODER 44 | ) 45 | ); 46 | oneOffSession = new OneOff<>(getSessionManager()); 47 | sessionTransport = HeaderST; 48 | } 49 | 50 | public static void main(String[] args) throws IOException { 51 | 52 | // ** akka-http boiler plate ** 53 | ActorSystem system = ActorSystem.create("example"); 54 | final ActorMaterializer materializer = ActorMaterializer.create(system); 55 | final Http http = Http.get(system); 56 | 57 | // ** akka-http-session setup ** 58 | final OptionalSessionDirective app = new OptionalSessionDirective(); 59 | 60 | // ** akka-http boiler plate continued ** 61 | final Flow routes = app.createRoutes().flow(system, materializer); 62 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 63 | 64 | System.out.println("Server started, press enter to stop"); 65 | System.in.read(); 66 | 67 | binding 68 | .thenCompose(ServerBinding::unbind) 69 | .thenAccept(unbound -> system.terminate()); 70 | } 71 | 72 | private Route createRoutes() { 73 | return 74 | route( 75 | pathPrefix("api", () -> 76 | route( 77 | path("do_login", () -> 78 | post(() -> 79 | entity(Unmarshaller.entityToString(), body -> { 80 | LOGGER.info("Logging in {}", body); 81 | return setSession(oneOffSession, sessionTransport, body, () -> 82 | extractRequestContext(ctx -> 83 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 84 | complete("ok") 85 | ) 86 | ) 87 | ); 88 | } 89 | ) 90 | ) 91 | ), 92 | 93 | path("current_login", () -> 94 | get(() -> 95 | optionalSession(oneOffSession, sessionTransport, session -> 96 | extractRequestContext(ctx -> { 97 | LOGGER.info("Current session: " + session); 98 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 99 | complete(session.orElse("no session")) 100 | ); 101 | } 102 | ) 103 | ) 104 | ) 105 | ) 106 | ) 107 | ) 108 | ); 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/java/session/directives/SessionDirective.java: -------------------------------------------------------------------------------- 1 | package session.directives; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SessionResult; 20 | import com.softwaremill.session.SetSessionTransport; 21 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 22 | import com.softwaremill.session.javadsl.SessionSerializers; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.io.IOException; 27 | import java.util.concurrent.CompletionStage; 28 | 29 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 30 | 31 | 32 | public class SessionDirective extends HttpSessionAwareDirectives { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(SessionDirective.class); 35 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 36 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 37 | 38 | private OneOff oneOffSession; 39 | private SetSessionTransport sessionTransport; 40 | 41 | private SessionDirective() { 42 | super(new SessionManager<>( 43 | SessionConfig.defaultConfig(SECRET), 44 | BASIC_ENCODER 45 | ) 46 | ); 47 | oneOffSession = new OneOff<>(getSessionManager()); 48 | sessionTransport = HeaderST; 49 | } 50 | 51 | public static void main(String[] args) throws IOException { 52 | 53 | // ** akka-http boiler plate ** 54 | ActorSystem system = ActorSystem.create("example"); 55 | final ActorMaterializer materializer = ActorMaterializer.create(system); 56 | final Http http = Http.get(system); 57 | 58 | // ** akka-http-session setup ** 59 | final SessionDirective app = new SessionDirective(); 60 | 61 | // ** akka-http boiler plate continued ** 62 | final Flow routes = app.createRoutes().flow(system, materializer); 63 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 64 | 65 | System.out.println("Server started, press enter to stop"); 66 | System.in.read(); 67 | 68 | binding 69 | .thenCompose(ServerBinding::unbind) 70 | .thenAccept(unbound -> system.terminate()); 71 | } 72 | 73 | private Route createRoutes() { 74 | return 75 | route( 76 | pathPrefix("api", () -> 77 | route( 78 | path("do_login", () -> 79 | post(() -> 80 | entity(Unmarshaller.entityToString(), body -> { 81 | LOGGER.info("Logging in {}", body); 82 | return setSession(oneOffSession, sessionTransport, body, () -> 83 | extractRequestContext(ctx -> 84 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 85 | complete("ok") 86 | ) 87 | ) 88 | ); 89 | } 90 | ) 91 | ) 92 | ), 93 | 94 | path("current_login", () -> 95 | get(() -> 96 | session(oneOffSession, sessionTransport, session -> 97 | extractRequestContext(ctx -> { 98 | LOGGER.info("Current session: " + session); 99 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> { 100 | if (SessionResult.NoSession$.MODULE$.equals(session)) 101 | return complete("no session"); 102 | return complete(session.toOption().get()); 103 | } 104 | ); 105 | } 106 | ) 107 | ) 108 | ) 109 | ) 110 | ) 111 | ) 112 | ); 113 | } 114 | } -------------------------------------------------------------------------------- /src/main/java/session/directives/TouchRequiredSessionDirective.java: -------------------------------------------------------------------------------- 1 | package session.directives; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import com.softwaremill.session.javadsl.SessionSerializers; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.util.concurrent.CompletionStage; 27 | 28 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 29 | 30 | 31 | public class TouchRequiredSessionDirective extends HttpSessionAwareDirectives { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(TouchRequiredSessionDirective.class); 34 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 35 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 36 | 37 | private OneOff oneOffSession; 38 | private SetSessionTransport sessionTransport; 39 | 40 | private TouchRequiredSessionDirective() { 41 | super(new SessionManager<>( 42 | SessionConfig.defaultConfig(SECRET), 43 | BASIC_ENCODER 44 | ) 45 | ); 46 | oneOffSession = new OneOff<>(getSessionManager()); 47 | 48 | // ***************************************************** // 49 | // This is where the Session Transport is set to Headers // 50 | // ***************************************************** // 51 | sessionTransport = HeaderST; 52 | } 53 | 54 | public static void main(String[] args) throws IOException { 55 | 56 | // ** akka-http boiler plate ** 57 | ActorSystem system = ActorSystem.create("example"); 58 | final ActorMaterializer materializer = ActorMaterializer.create(system); 59 | final Http http = Http.get(system); 60 | 61 | // ** akka-http-session setup ** 62 | final TouchRequiredSessionDirective app = new TouchRequiredSessionDirective(); 63 | 64 | // ** akka-http boiler plate continued ** 65 | final Flow routes = app.createRoutes().flow(system, materializer); 66 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 67 | 68 | System.out.println("Server started, press enter to stop"); 69 | System.in.read(); 70 | 71 | binding 72 | .thenCompose(ServerBinding::unbind) 73 | .thenAccept(unbound -> system.terminate()); 74 | } 75 | 76 | private Route createRoutes() { 77 | return 78 | route( 79 | pathPrefix("api", () -> 80 | route( 81 | path("do_login", () -> 82 | post(() -> 83 | entity(Unmarshaller.entityToString(), body -> { 84 | LOGGER.info("Logging in {}", body); 85 | return setSession(oneOffSession, sessionTransport, body, () -> 86 | extractRequestContext(ctx -> 87 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 88 | complete("ok") 89 | ) 90 | ) 91 | ); 92 | } 93 | ) 94 | ) 95 | ), 96 | 97 | // This should be protected and accessible only when logged in 98 | path("touch", () -> 99 | post(() -> 100 | touchRequiredSession(oneOffSession, sessionTransport, session -> 101 | extractRequestContext(ctx -> { 102 | LOGGER.info("Touching {}", session); 103 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 104 | complete("session touched") 105 | ); 106 | } 107 | ) 108 | ) 109 | ) 110 | ), 111 | 112 | // This should be protected and accessible only when logged in 113 | path("current_login", () -> 114 | get(() -> 115 | requiredSession(oneOffSession, sessionTransport, session -> 116 | extractRequestContext(ctx -> { 117 | LOGGER.info("Current session: " + session); 118 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 119 | complete(session) 120 | ); 121 | } 122 | ) 123 | ) 124 | ) 125 | ) 126 | ) 127 | ) 128 | ); 129 | } 130 | } -------------------------------------------------------------------------------- /src/main/java/session/jwt/JwtEncodedSession.java: -------------------------------------------------------------------------------- 1 | package session.jwt; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.JwtSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import com.softwaremill.session.javadsl.JwtSessionSerializers; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.util.concurrent.CompletionStage; 27 | 28 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 29 | 30 | 31 | public class JwtEncodedSession extends HttpSessionAwareDirectives { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(JwtEncodedSession.class); 34 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 35 | 36 | // ***************************************************** // 37 | // This is where the Session Encoder is set to JWT // 38 | // ***************************************************** // 39 | private static final SessionEncoder JWT_ENCODER = new JwtSessionEncoder<>(JwtSessionSerializers.StringToJValueSessionSerializer, JwtSessionSerializers.DefaultUtcDateFormat); 40 | 41 | private OneOff oneOffSession; 42 | private SetSessionTransport sessionTransport; 43 | 44 | private JwtEncodedSession() { 45 | super(new SessionManager<>( 46 | SessionConfig.defaultConfig(SECRET), 47 | JWT_ENCODER 48 | ) 49 | ); 50 | oneOffSession = new OneOff<>(getSessionManager()); 51 | sessionTransport = HeaderST; 52 | } 53 | 54 | public static void main(String[] args) throws IOException { 55 | 56 | // ** akka-http boiler plate ** 57 | ActorSystem system = ActorSystem.create("example"); 58 | final ActorMaterializer materializer = ActorMaterializer.create(system); 59 | final Http http = Http.get(system); 60 | 61 | // ** akka-http-session setup ** 62 | final JwtEncodedSession app = new JwtEncodedSession(); 63 | 64 | // ** akka-http boiler plate continued ** 65 | final Flow routes = app.createRoutes().flow(system, materializer); 66 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 67 | 68 | System.out.println("Server started, press enter to stop"); 69 | System.in.read(); 70 | 71 | binding 72 | .thenCompose(ServerBinding::unbind) 73 | .thenAccept(unbound -> system.terminate()); 74 | } 75 | 76 | private Route createRoutes() { 77 | return 78 | route( 79 | pathPrefix("api", () -> 80 | route( 81 | path("do_login", () -> 82 | post(() -> 83 | entity(Unmarshaller.entityToString(), body -> { 84 | LOGGER.info("Logging in {}", body); 85 | return setSession(oneOffSession, sessionTransport, body, () -> 86 | extractRequestContext(ctx -> 87 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 88 | complete("ok") 89 | ) 90 | ) 91 | ); 92 | } 93 | ) 94 | ) 95 | ), 96 | 97 | // This should be protected and accessible only when logged in 98 | path("do_logout", () -> 99 | post(() -> 100 | requiredSession(oneOffSession, sessionTransport, session -> 101 | invalidateSession(oneOffSession, sessionTransport, () -> 102 | extractRequestContext(ctx -> { 103 | LOGGER.info("Logging out {}", session); 104 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 105 | complete("ok") 106 | ); 107 | } 108 | ) 109 | ) 110 | ) 111 | ) 112 | ), 113 | 114 | // This should be protected and accessible only when logged in 115 | path("current_login", () -> 116 | get(() -> 117 | requiredSession(oneOffSession, sessionTransport, session -> 118 | extractRequestContext(ctx -> { 119 | LOGGER.info("Current session: " + session); 120 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 121 | complete(session) 122 | ); 123 | } 124 | ) 125 | ) 126 | ) 127 | ) 128 | ) 129 | ), 130 | pathPrefix("site", () -> 131 | getFromResourceDirectory("")) 132 | ); 133 | } 134 | } -------------------------------------------------------------------------------- /src/main/java/session/transport/CookieTransport.java: -------------------------------------------------------------------------------- 1 | package session.transport; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.model.StatusCodes; 11 | import akka.http.javadsl.model.Uri; 12 | import akka.http.javadsl.server.Route; 13 | import akka.http.javadsl.unmarshalling.Unmarshaller; 14 | import akka.stream.ActorMaterializer; 15 | import akka.stream.javadsl.Flow; 16 | import com.softwaremill.session.BasicSessionEncoder; 17 | import com.softwaremill.session.OneOff; 18 | import com.softwaremill.session.SessionConfig; 19 | import com.softwaremill.session.SessionEncoder; 20 | import com.softwaremill.session.SessionManager; 21 | import com.softwaremill.session.SetSessionTransport; 22 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 23 | import com.softwaremill.session.javadsl.SessionSerializers; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import java.io.IOException; 28 | import java.util.concurrent.CompletionStage; 29 | 30 | import static com.softwaremill.session.javadsl.SessionTransports.CookieST; 31 | 32 | 33 | public class CookieTransport extends HttpSessionAwareDirectives { 34 | 35 | private static final Logger LOGGER = LoggerFactory.getLogger(CookieTransport.class); 36 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 37 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 38 | 39 | private OneOff oneOffSession; 40 | private SetSessionTransport sessionTransport; 41 | 42 | private CookieTransport() { 43 | super(new SessionManager<>( 44 | SessionConfig.defaultConfig(SECRET), 45 | BASIC_ENCODER 46 | ) 47 | ); 48 | oneOffSession = new OneOff<>(getSessionManager()); 49 | 50 | // ***************************************************** // 51 | // This is where the Session Transport is set to Cookies // 52 | // ***************************************************** // 53 | sessionTransport = CookieST; 54 | } 55 | 56 | public static void main(String[] args) throws IOException { 57 | 58 | // ** akka-http boiler plate ** 59 | ActorSystem system = ActorSystem.create("example"); 60 | final ActorMaterializer materializer = ActorMaterializer.create(system); 61 | final Http http = Http.get(system); 62 | 63 | // ** akka-http-session setup ** 64 | final CookieTransport app = new CookieTransport(); 65 | 66 | // ** akka-http boiler plate continued ** 67 | final Flow routes = app.createRoutes().flow(system, materializer); 68 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 69 | 70 | System.out.println("Server started, press enter to stop"); 71 | System.in.read(); 72 | 73 | binding 74 | .thenCompose(ServerBinding::unbind) 75 | .thenAccept(unbound -> system.terminate()); 76 | } 77 | 78 | private Route createRoutes() { 79 | return 80 | route( 81 | pathSingleSlash(() -> 82 | redirect(Uri.create("/site/index.html"), StatusCodes.FOUND) 83 | ), 84 | route( 85 | pathPrefix("api", () -> 86 | route( 87 | path("do_login", () -> 88 | post(() -> 89 | entity(Unmarshaller.entityToString(), body -> { 90 | LOGGER.info("Logging in {}", body); 91 | return setSession(oneOffSession, sessionTransport, body, () -> 92 | extractRequestContext(ctx -> 93 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 94 | complete("ok") 95 | ) 96 | ) 97 | ); 98 | } 99 | ) 100 | ) 101 | ), 102 | 103 | // This should be protected and accessible only when logged in 104 | path("do_logout", () -> 105 | post(() -> 106 | requiredSession(oneOffSession, sessionTransport, session -> 107 | invalidateSession(oneOffSession, sessionTransport, () -> 108 | extractRequestContext(ctx -> { 109 | LOGGER.info("Logging out {}", session); 110 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 111 | complete("ok") 112 | ); 113 | } 114 | ) 115 | ) 116 | ) 117 | ) 118 | ), 119 | 120 | // This should be protected and accessible only when logged in 121 | path("current_login", () -> 122 | get(() -> 123 | requiredSession(oneOffSession, sessionTransport, session -> 124 | extractRequestContext(ctx -> { 125 | LOGGER.info("Current session: " + session); 126 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 127 | complete(session) 128 | ); 129 | } 130 | ) 131 | ) 132 | ) 133 | ) 134 | ) 135 | ), 136 | pathPrefix("site", () -> 137 | getFromResourceDirectory("")) 138 | ) 139 | ); 140 | } 141 | } -------------------------------------------------------------------------------- /src/main/java/session/transport/HeaderTransport.java: -------------------------------------------------------------------------------- 1 | package session.transport; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.http.javadsl.ConnectHttp; 6 | import akka.http.javadsl.Http; 7 | import akka.http.javadsl.ServerBinding; 8 | import akka.http.javadsl.model.HttpRequest; 9 | import akka.http.javadsl.model.HttpResponse; 10 | import akka.http.javadsl.server.Route; 11 | import akka.http.javadsl.unmarshalling.Unmarshaller; 12 | import akka.stream.ActorMaterializer; 13 | import akka.stream.javadsl.Flow; 14 | import com.softwaremill.session.BasicSessionEncoder; 15 | import com.softwaremill.session.OneOff; 16 | import com.softwaremill.session.SessionConfig; 17 | import com.softwaremill.session.SessionEncoder; 18 | import com.softwaremill.session.SessionManager; 19 | import com.softwaremill.session.SetSessionTransport; 20 | import com.softwaremill.session.javadsl.HttpSessionAwareDirectives; 21 | import com.softwaremill.session.javadsl.SessionSerializers; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.util.concurrent.CompletionStage; 27 | 28 | import static com.softwaremill.session.javadsl.SessionTransports.HeaderST; 29 | 30 | 31 | public class HeaderTransport extends HttpSessionAwareDirectives { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(HeaderTransport.class); 34 | private static final String SECRET = "c05ll3lesrinf39t7mc5h6un6r0c69lgfno69dsak3vabeqamouq4328cuaekros401ajdpkh60rrtpd8ro24rbuqmgtnd1ebag6ljnb65i8a55d482ok7o0nch0bfbe"; 35 | private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(SessionSerializers.StringToStringSessionSerializer); 36 | 37 | private OneOff oneOffSession; 38 | private SetSessionTransport sessionTransport; 39 | 40 | private HeaderTransport() { 41 | super(new SessionManager<>( 42 | SessionConfig.defaultConfig(SECRET), 43 | BASIC_ENCODER 44 | ) 45 | ); 46 | oneOffSession = new OneOff<>(getSessionManager()); 47 | 48 | // ***************************************************** // 49 | // This is where the Session Transport is set to Headers // 50 | // ***************************************************** // 51 | sessionTransport = HeaderST; 52 | } 53 | 54 | public static void main(String[] args) throws IOException { 55 | 56 | // ** akka-http boiler plate ** 57 | ActorSystem system = ActorSystem.create("example"); 58 | final ActorMaterializer materializer = ActorMaterializer.create(system); 59 | final Http http = Http.get(system); 60 | 61 | // ** akka-http-session setup ** 62 | final HeaderTransport app = new HeaderTransport(); 63 | 64 | // ** akka-http boiler plate continued ** 65 | final Flow routes = app.createRoutes().flow(system, materializer); 66 | final CompletionStage binding = http.bindAndHandle(routes, ConnectHttp.toHost("localhost", 8080), materializer); 67 | 68 | System.out.println("Server started, press enter to stop"); 69 | System.in.read(); 70 | 71 | binding 72 | .thenCompose(ServerBinding::unbind) 73 | .thenAccept(unbound -> system.terminate()); 74 | } 75 | 76 | private Route createRoutes() { 77 | return 78 | route( 79 | pathPrefix("api", () -> 80 | route( 81 | path("do_login", () -> 82 | post(() -> 83 | entity(Unmarshaller.entityToString(), body -> { 84 | LOGGER.info("Logging in {}", body); 85 | return setSession(oneOffSession, sessionTransport, body, () -> 86 | extractRequestContext(ctx -> 87 | onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 88 | complete("ok") 89 | ) 90 | ) 91 | ); 92 | } 93 | ) 94 | ) 95 | ), 96 | 97 | // This should be protected and accessible only when logged in 98 | path("do_logout", () -> 99 | post(() -> 100 | requiredSession(oneOffSession, sessionTransport, session -> 101 | invalidateSession(oneOffSession, sessionTransport, () -> 102 | extractRequestContext(ctx -> { 103 | LOGGER.info("Logging out {}", session); 104 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 105 | complete("ok") 106 | ); 107 | } 108 | ) 109 | ) 110 | ) 111 | ) 112 | ), 113 | 114 | // This should be protected and accessible only when logged in 115 | path("current_login", () -> 116 | get(() -> 117 | requiredSession(oneOffSession, sessionTransport, session -> 118 | extractRequestContext(ctx -> { 119 | LOGGER.info("Current session: " + session); 120 | return onSuccess(() -> ctx.completeWith(HttpResponse.create()), routeResult -> 121 | complete(session) 122 | ); 123 | } 124 | ) 125 | ) 126 | ) 127 | ) 128 | ) 129 | ) 130 | ); 131 | } 132 | } -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka.http.session { 2 | cookie { 3 | name = "_sessiondata" 4 | domain = none 5 | path = / 6 | secure = false 7 | http-only = true 8 | } 9 | max-age = 10 minutes 10 | encrypt-data = false 11 | 12 | csrf { 13 | cookie { 14 | name = "XSRF-TOKEN" 15 | domain = none 16 | path = / 17 | secure = false 18 | http-only = false 19 | } 20 | submitted-name = "X-XSRF-TOKEN" 21 | } 22 | 23 | refresh-token { 24 | cookie { 25 | name = "_refreshtoken" 26 | domain = none 27 | path = / 28 | secure = false 29 | http-only = true 30 | } 31 | header { 32 | send-to-client-name = "Set-Refresh-Token" 33 | get-from-client-name = "Refresh-Token" 34 | } 35 | max-age = 30 days 36 | remove-used-token-after = 5 seconds 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

akka-http-session example

9 | 10 |

11 | Logged in as: 12 |

13 |

14 | Get login request status: 15 |

16 | 17 |
18 |

19 | Login: 20 |

21 | 22 |
23 | 24 |
25 | 26 |
27 | 28 | 89 | 90 | -------------------------------------------------------------------------------- /src/main/resources/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ 2 | !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; 3 | if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("