pax_global_header00006660000000000000000000000064150063652010014510gustar00rootroot0000000000000052 comment=96663ece1596a3462704c30f98b85cd7e0337d6e rack-session-2.1.1/000077500000000000000000000000001500636520100141125ustar00rootroot00000000000000rack-session-2.1.1/.contributors.yaml000066400000000000000000000476431500636520100176270ustar00rootroot00000000000000- time: 2022-02-15T18:41:48+13:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz - time: 2021-05-13T10:35:53-04:00 author: name: Michael Coyne email: mikeycgto@gmail.com - time: 2022-02-03T13:20:12-08:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2022-01-26T17:55:30-08:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-03-11T08:06:43-07:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-07-14T10:41:24-07:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-09-16T16:45:22-04:00 author: name: Alec Clarke email: alec.clarke@clio.com - time: 2020-06-28T12:51:51-04:00 author: name: Bart de Water email: bartdewater@gmail.com - time: 2020-05-27T19:41:59-07:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-02-24T14:34:28+09:00 author: name: Yudai Suzuki email: 3280467rec@gmail.com - time: 2020-02-10T17:33:15+09:00 author: name: Ryuta Kamizono email: kamipo@gmail.com - time: 2020-02-01T03:16:31+00:00 author: name: Alex Speller email: alex@alexspeller.com - time: 2020-01-27T14:30:11-08:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-01-27T12:33:40-08:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-01-16T12:44:12-08:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-01-11T22:48:15+01:00 author: name: Pavel Rosicky email: pavel.rosicky@easy.cz - time: 2019-12-16T10:36:52+02:00 author: name: Oleh Demianiuk email: oleh.demianiuk@managebac.com - time: 2020-01-10T16:25:47-08:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-01-11T10:58:12+13:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz - time: 2020-01-09T12:55:06-08:00 author: name: Jeremy Evans email: code@jeremyevans.net - time: 2020-01-09T11:21:43+13:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz - time: 2019-12-18T10:07:23-08:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-11-29T01:10:32+02:00 author: name: Dima Fatko email: fatkodima123@gmail.com - time: 2019-10-19T00:54:35+02:00 author: name: Pavel Rosicky email: pavel.rosicky@easy.cz - time: 2019-10-21T16:39:00-04:00 author: name: Rafael Mendonça França email: rafael@franca.dev - time: 2019-10-17T19:00:09+07:00 author: name: Adrian Setyadi email: a.styd@yahoo.com - time: 2019-10-16T14:07:36-04:00 author: name: Rafael Mendonça França email: rafael@franca.dev - time: 2019-10-09T19:14:08-04:00 author: name: Rafael Mendonça França email: rafael@franca.dev - time: 2019-10-09T18:06:23-04:00 author: name: Rafael Mendonça França email: rafael@franca.dev - time: 2019-10-09T17:50:45-04:00 author: name: Rafael Mendonça França email: rafael@franca.dev - time: 2019-10-09T23:16:00+07:00 author: name: Adrian Setyadi email: a.styd@yahoo.com - time: 2019-10-09T13:42:48+07:00 author: name: Adrian Setyadi email: a.styd@yahoo.com - time: 2019-09-30T06:42:41+07:00 author: name: Adrian Setyadi email: a.styd@yahoo.com - time: 2019-08-13T17:16:23-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-08-13T16:48:41-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-08-13T16:45:04-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-08-13T16:38:01-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-08-13T16:31:06-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-08-13T16:20:32-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-08-13T15:43:58-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-08-13T15:32:20-04:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2019-07-27T14:57:42+01:00 author: name: Frederick Cheung email: frederick.cheung@gmail.com - time: 2019-04-17T18:52:13+02:00 author: name: Krzysztof Rybka email: krzysztof.rybka@gmail.com - time: 2018-04-17T17:50:18+09:00 author: name: Yoshiyuki Hirano email: yhirano@me.com - time: 2018-04-17T02:41:39+09:00 author: name: Yoshiyuki Hirano email: yhirano@me.com - time: 2018-04-13T21:48:52-07:00 author: name: Dillon Welch email: daw0328@gmail.com - time: 2017-05-12T14:00:36-07:00 author: name: Jordan Raine email: jnraine@gmail.com - time: 2016-12-06T15:45:10+08:00 author: name: Jian Weihang email: tonytonyjan@gmail.com - time: 2016-10-24T14:47:14+02:00 author: name: Yann Vanhalewyn email: yann.vanhalewyn@gmail.com - time: 2016-09-09T21:59:10-04:00 author: name: Kir Shatrov email: kirs@users.noreply.github.com - time: 2015-12-10T13:03:35+01:00 author: name: Michael Sauter email: michael.sauter@experteer.com - time: 2015-10-29T13:03:04+09:00 author: name: Yuichiro Kaneko email: spiketeika@gmail.com - time: 2015-10-11T02:21:42+02:00 author: name: Francesco Rodríguez email: frodsan@me.com - time: 2015-09-25T12:11:05-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-09-25T11:24:59-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-09-24T16:00:40-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-09-24T15:53:14-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-09-13T10:59:04-07:00 author: name: David Runger email: daverunger@gmail.com - time: 2015-09-05T11:19:00-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-09-04T18:57:29-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-09-04T16:11:16-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-09-03T07:15:12+02:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-08-27T10:22:02-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-08-22T17:51:36-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-08-22T16:45:38-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-08-22T21:11:38+02:00 author: name: deepj email: deepjungle.maca@gmail.com - time: 2015-06-18T17:54:07-07:00 author: name: Doug McInnes email: doug@dougmcinnes.com - time: 2015-06-13T21:37:19-03:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2015-06-12T14:17:35-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2015-06-12T23:10:39+02:00 author: name: deepj email: deepjungle.maca@gmail.com - time: 2015-06-11T19:05:19-07:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2014-07-08T12:18:40+02:00 author: name: Michal Bryxí email: michal.bryxi@gmail.com - time: 2014-06-12T23:37:19+02:00 author: name: Michal Bryxí email: michal.bryxi@gmail.com - time: 2014-01-10T13:00:14-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-12-06T01:10:37-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-12-05T20:32:08-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-12-05T11:49:34-08:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2013-09-16T19:12:28+02:00 author: name: Charles Hornberger email: charles.hornberger@gmail.com - time: 2013-09-16T09:23:02+02:00 author: name: Charles Hornberger email: charles.hornberger@gmail.com - time: 2013-05-24T01:47:29+05:30 author: name: Vipul A M email: vipulnsward@gmail.com - time: 2013-04-29T11:55:32-07:00 author: name: James Tucker email: jftucker@gmail.com - time: 2013-04-13T11:22:33+05:30 author: name: Vipul A M email: vipulnsward@gmail.com - time: 2013-02-07T14:47:10-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2013-02-06T14:13:10-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2013-02-05T17:43:10-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2013-01-30T02:56:58-08:00 author: name: Postmodern email: postmodern.mod3@gmail.com - time: 2013-01-29T12:01:44-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-01-28T13:44:57-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2013-01-28T13:30:52-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2013-01-24T21:02:23-08:00 author: name: Andrew Cole email: aocole@gmail.com - time: 2013-01-11T01:57:54-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-01-10T12:03:34-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-01-10T00:53:40-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-01-10T00:44:02-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2013-01-09T00:59:10-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2012-11-03T10:30:31-07:00 author: name: James Tucker email: raggi@google.com - time: 2012-11-03T10:29:00-07:00 author: name: James Tucker email: jftucker@gmail.com - time: 2012-11-02T09:48:52-07:00 author: name: James Tucker email: raggi@google.com - time: 2012-10-15T22:22:22-02:00 author: name: Santiago Pastorino email: santiago@wyeworks.com - time: 2012-07-19T12:46:50-07:00 author: name: Jamie Macey email: jamie@tracefunc.com - time: 2012-03-18T19:20:54-07:00 author: name: James Tucker email: jftucker@gmail.com - time: 2012-03-18T18:36:31-07:00 author: name: James Tucker email: jftucker@gmail.com - time: 2012-03-18T02:50:05+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-18T16:19:10+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-18T16:15:45+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-18T15:44:06+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-18T13:22:43+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-18T13:21:02+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-18T12:47:53+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-04T01:44:04+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-02-02T04:23:20-08:00 author: name: Konstantin Haase email: k.haase@finn.de - time: 2012-01-30T14:23:45-08:00 author: name: Timothy Elliott email: tle@holymonkey.com - time: 2012-01-27T15:08:08+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-27T15:07:08+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-27T14:56:16+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-27T12:24:02+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-27T12:19:42+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-21T15:48:16-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2012-01-18T19:05:36+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-18T19:00:20+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-18T18:23:24+04:00 author: name: Ravil Bayramgalin email: brainopia@evilmartians.com - time: 2012-01-16T17:08:42+11:00 author: name: Yun Huang Yong email: gumby@mooh.org - time: 2012-01-07T14:51:59-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2011-12-27T19:39:42+01:00 author: name: José Valim email: jose.valim@gmail.com - time: 2011-12-17T14:41:39-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2011-12-17T13:52:22-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2011-12-16T19:03:58-08:00 author: name: John Manoogian III email: jm3@jm3.net - time: 2011-12-04T15:42:32-08:00 author: name: James Tucker email: jftucker@gmail.com - time: 2011-11-30T12:26:44+01:00 author: name: José Valim email: jose.valim@gmail.com - time: 2011-11-12T19:09:50-08:00 author: name: Will Leinweber email: will@bitfission.com - time: 2011-07-16T14:54:43+02:00 author: name: Konstantin Haase email: konstantin.mailinglists@googlemail.com - time: 2011-06-09T09:14:04+02:00 author: name: Konstantin Haase email: konstantin.mailinglists@googlemail.com - time: 2011-05-31T11:15:42+02:00 author: name: Konstantin Haase email: konstantin.mailinglists@googlemail.com - time: 2011-05-08T11:38:12-07:00 author: name: James Tucker email: jftucker@gmail.com - time: 2011-05-08T11:37:57-07:00 author: name: James Tucker email: jftucker@gmail.com - time: 2011-05-04T20:36:37+02:00 author: name: José Valim email: jose.valim@gmail.com - time: 2011-05-04T12:04:45+02:00 author: name: Konstantin Haase email: konstantin.mailinglists@googlemail.com - time: 2011-05-04T11:19:35+02:00 author: name: Konstantin Haase email: konstantin.mailinglists@googlemail.com - time: 2011-05-03T14:50:56+02:00 author: name: Konstantin Haase email: konstantin.mailinglists@googlemail.com - time: 2011-01-24T19:11:37-05:00 author: name: Max Cantor email: max@maxcantor.net - time: 2010-12-18T06:01:13+08:00 author: name: Aaron Patterson email: aaron.patterson@gmail.com - time: 2010-10-03T14:47:55-07:00 author: name: José Valim email: jose.valim@gmail.com - time: 2010-10-03T17:32:02-03:00 author: name: James Tucker email: jftucker@gmail.com - time: 2010-10-03T19:30:20+02:00 author: name: José Valim email: jose.valim@gmail.com - time: 2010-09-28T14:16:49+02:00 author: name: José Valim email: jose.valim@gmail.com - time: 2010-09-19T23:26:16+02:00 author: name: José Valim email: jose.valim@gmail.com - time: 2010-07-19T12:41:56+02:00 author: name: José Valim email: jose.valim@gmail.com - time: 2010-07-19T11:09:46+02:00 author: name: José Valim email: jose.valim@gmail.com - time: 2010-05-05T11:54:07-06:00 author: name: Simon Chiang email: simon.chiang@pinnacol.com - time: 2009-12-03T13:07:46-08:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2009-11-22T20:15:28-08:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2009-11-22T18:08:53-08:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2009-11-24T20:35:04+08:00 author: name: Mickaël Riga email: mig@mypeplum.com - time: 2009-08-05T11:01:43-05:00 author: name: Joshua Peek email: josh@joshpeek.com - time: 2009-08-03T16:03:30-05:00 author: name: Joshua Peek email: josh@joshpeek.com - time: 2009-08-03T12:02:37-05:00 author: name: Joshua Peek email: josh@joshpeek.com - time: 2009-01-16T14:53:58-08:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2009-01-10T11:18:01-08:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2009-01-14T12:38:51-06:00 author: name: Joshua Peek email: josh@joshpeek.com - time: 2009-01-07T21:15:44-08:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-11-19T22:07:38+01:00 author: name: Daniel Roethlisberger email: daniel@roe.ch - time: 2008-11-19T22:23:30+01:00 author: name: Daniel Roethlisberger email: daniel@roe.ch - time: 2008-09-30T19:18:35+02:00 author: name: Leah Neukirchen email: leah@vuxu.org - time: 2008-08-07T03:32:31-07:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-06-28T14:37:09-07:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-06-03T21:55:55-07:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-04-26T21:37:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-29T04:32:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-25T11:15:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-19T11:43:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-17T15:59:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-17T11:19:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-17T09:12:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T14:30:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T11:55:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T09:01:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T08:33:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T08:26:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T08:23:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T08:21:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-16T04:59:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-14T23:57:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-11T12:02:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-11T11:59:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-11T11:56:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-11T11:52:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-11T11:29:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-11T11:25:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2008-03-11T11:11:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2007-12-31T18:34:00+00:00 author: name: Leah Neukirchen email: leah@vuxu.org - time: 2007-11-18T19:20:00+00:00 author: name: Leah Neukirchen email: leah@vuxu.org - time: 2007-11-18T05:08:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2007-08-28T23:14:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2007-08-11T17:28:00+00:00 author: name: Scytrin dai Kinthra email: scytrin@gmail.com - time: 2007-05-16T14:53:00+00:00 author: name: Leah Neukirchen email: leah@vuxu.org - time: 2007-03-12T16:04:00+00:00 author: name: Leah Neukirchen email: leah@vuxu.org - time: 2007-03-10T14:38:00+00:00 author: name: Leah Neukirchen email: leah@vuxu.org - time: 2007-03-09T23:40:00+00:00 author: name: Leah Neukirchen email: leah@vuxu.org rack-session-2.1.1/.github/000077500000000000000000000000001500636520100154525ustar00rootroot00000000000000rack-session-2.1.1/.github/dependabot.yml000066400000000000000000000001661500636520100203050ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'weekly' rack-session-2.1.1/.github/workflows/000077500000000000000000000000001500636520100175075ustar00rootroot00000000000000rack-session-2.1.1/.github/workflows/test-external.yaml000066400000000000000000000011461500636520100231740ustar00rootroot00000000000000name: Test External on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu ruby: - "3.1" - "3.2" - "3.3" - "3.4" steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test:external rack-session-2.1.1/.github/workflows/test.yaml000066400000000000000000000021671500636520100213600ustar00rootroot00000000000000name: Test on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos ruby: - "2.5" - "2.6" - "2.7" - "3.0" - "3.1" - "3.2" - "3.3" - "3.4" experimental: [false] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true exclude: # Not supported on macos-arm64: - os: macos ruby: "2.5" steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec rake test rack-session-2.1.1/.gitignore000066400000000000000000000000641500636520100161020ustar00rootroot00000000000000/.bundle/ /pkg/ /gems.locked /.covered.db /external rack-session-2.1.1/Rakefile000066400000000000000000000004211500636520100155540ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/gem_tasks" require "rake/testtask" desc "Run all the tests" task default: :test Rake::TestTask.new("test") do |t| t.libs << "test" t.test_files = FileList["test/**/spec_*.rb"] t.warning = false t.verbose = true end rack-session-2.1.1/bake.rb000066400000000000000000000005001500636520100153340ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024, by Samuel Williams. # Update the project documentation with the new version number. # # @parameter version [String] The new version number. def after_gem_release_version_increment(version) context["releases:update"].call(version) end rack-session-2.1.1/config/000077500000000000000000000000001500636520100153575ustar00rootroot00000000000000rack-session-2.1.1/config/external.yaml000066400000000000000000000001171500636520100200640ustar00rootroot00000000000000rack: url: https://github.com/rack/rack.git command: bundle exec rake test rack-session-2.1.1/gems.rb000066400000000000000000000007371500636520100154010ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. source 'https://rubygems.org' gemspec group :maintenance, optional: true do if RUBY_VERSION > "3.1" gem "bake" gem "bake-gem" gem "bake-modernize" gem "bake-releases" end gem "rubocop", require: false gem "rubocop-packaging", require: false end group :doc do gem 'rdoc' end group :test do gem "bake-test" gem "bake-test-external" end rack-session-2.1.1/lib/000077500000000000000000000000001500636520100146605ustar00rootroot00000000000000rack-session-2.1.1/lib/rack/000077500000000000000000000000001500636520100156005ustar00rootroot00000000000000rack-session-2.1.1/lib/rack/session.rb000066400000000000000000000004751500636520100176160ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. # Copyright, 2022, by Jeremy Evans. module Rack module Session autoload :Cookie, "rack/session/cookie" autoload :Pool, "rack/session/pool" autoload :Memcache, "rack/session/memcache" end end rack-session-2.1.1/lib/rack/session/000077500000000000000000000000001500636520100172635ustar00rootroot00000000000000rack-session-2.1.1/lib/rack/session/abstract/000077500000000000000000000000001500636520100210665ustar00rootroot00000000000000rack-session-2.1.1/lib/rack/session/abstract/id.rb000066400000000000000000000362251500636520100220170ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. # Copyright, 2022, by Jeremy Evans. require 'time' require 'securerandom' require 'digest/sha2' require 'rack/constants' require 'rack/request' require 'rack/response' require_relative '../constants' module Rack module Session class SessionId ID_VERSION = 2 attr_reader :public_id def initialize(public_id) @public_id = public_id end def private_id "#{ID_VERSION}::#{hash_sid(public_id)}" end alias :cookie_value :public_id alias :to_s :public_id def empty?; false; end def inspect; public_id.inspect; end private def hash_sid(sid) Digest::SHA256.hexdigest(sid) end end module Abstract # SessionHash is responsible to lazily load the session from store. class SessionHash include Enumerable attr_writer :id Unspecified = Object.new def self.find(req) req.get_header RACK_SESSION end def self.set(req, session) req.set_header RACK_SESSION, session end def self.set_options(req, options) req.set_header RACK_SESSION_OPTIONS, options.dup end def initialize(store, req) @store = store @req = req @loaded = false end def id return @id if @loaded or instance_variable_defined?(:@id) @id = @store.send(:extract_session_id, @req) end def options @req.session_options end def each(&block) load_for_read! @data.each(&block) end def [](key) load_for_read! @data[key.to_s] end def dig(key, *keys) load_for_read! @data.dig(key.to_s, *keys) end def fetch(key, default = Unspecified, &block) load_for_read! if default == Unspecified @data.fetch(key.to_s, &block) else @data.fetch(key.to_s, default, &block) end end def has_key?(key) load_for_read! @data.has_key?(key.to_s) end alias :key? :has_key? alias :include? :has_key? def []=(key, value) load_for_write! @data[key.to_s] = value end alias :store :[]= def clear load_for_write! @data.clear end def destroy clear @id = @store.send(:delete_session, @req, id, options) end def to_hash load_for_read! @data.dup end def update(hash) load_for_write! @data.update(stringify_keys(hash)) end alias :merge! :update def replace(hash) load_for_write! @data.replace(stringify_keys(hash)) end def delete(key) load_for_write! @data.delete(key.to_s) end def inspect if loaded? @data.inspect else "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>" end end def exists? return @exists if instance_variable_defined?(:@exists) @data = {} @exists = @store.send(:session_exists?, @req) end def loaded? @loaded end def empty? load_for_read! @data.empty? end def keys load_for_read! @data.keys end def values load_for_read! @data.values end private def load_for_read! load! if !loaded? && exists? end def load_for_write! load! unless loaded? end def load! @id, session = @store.send(:load_session, @req) @data = stringify_keys(session) @loaded = true end def stringify_keys(other) # Use transform_keys after dropping Ruby 2.4 support hash = {} other.to_hash.each do |key, value| hash[key.to_s] = value end hash end end # ID sets up a basic framework for implementing an id based sessioning # service. Cookies sent to the client for maintaining sessions will only # contain an id reference. Only #find_session, #write_session and # #delete_session are required to be overwritten. # # All parameters are optional. # * :key determines the name of the cookie, by default it is # 'rack.session' # * :path, :domain, :expire_after, :secure, :httponly, :partitioned and :same_site set # the related cookie options as by Rack::Response#set_cookie # * :skip will not a set a cookie in the response nor update the session state # * :defer will not set a cookie in the response but still update the session # state if it is used with a backend # * :renew (implementation dependent) will prompt the generation of a new # session id, and migration of data to be referenced at the new id. If # :defer is set, it will be overridden and the cookie will be set. # * :sidbits sets the number of bits in length that a generated session # id will be. # # These options can be set on a per request basis, at the location of # env['rack.session.options']. Additionally the id of the # session can be found within the options hash at the key :id. It is # highly not recommended to change its value. # # Is Rack::Utils::Context compatible. # # Not included by default; you must require 'rack/session/abstract/id' # to use. class Persisted DEFAULT_OPTIONS = { key: RACK_SESSION, path: '/', domain: nil, expire_after: nil, secure: false, httponly: true, partitioned: false, defer: false, renew: false, sidbits: 128, cookie_only: true, secure_random: ::SecureRandom }.freeze attr_reader :key, :default_options, :sid_secure, :same_site def initialize(app, options = {}) @app = app @default_options = self.class::DEFAULT_OPTIONS.merge(options) @key = @default_options.delete(:key) @assume_ssl = @default_options.delete(:assume_ssl) @cookie_only = @default_options.delete(:cookie_only) @same_site = @default_options.delete(:same_site) initialize_sid end def call(env) context(env) end def context(env, app = @app) req = make_request env prepare_session(req) status, headers, body = app.call(req.env) res = Rack::Response::Raw.new status, headers commit_session(req, res) [status, headers, body] end private def make_request(env) Rack::Request.new env end def initialize_sid @sidbits = @default_options[:sidbits] @sid_secure = @default_options[:secure_random] @sid_length = @sidbits / 4 end # Generate a new session id using Ruby #rand. The size of the # session id is controlled by the :sidbits option. # Monkey patch this to use custom methods for session id generation. def generate_sid(secure = @sid_secure) if secure secure.hex(@sid_length) else "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1) end rescue NotImplementedError generate_sid(false) end # Sets the lazy session at 'rack.session' and places options and session # metadata into 'rack.session.options'. def prepare_session(req) session_was = req.get_header RACK_SESSION session = session_class.new(self, req) req.set_header RACK_SESSION, session req.set_header RACK_SESSION_OPTIONS, @default_options.dup session.merge! session_was if session_was end # Extracts the session id from provided cookies and passes it and the # environment to #find_session. def load_session(req) sid = current_session_id(req) sid, session = find_session(req, sid) [sid, session || {}] end # Extract session id from request object. def extract_session_id(request) sid = request.cookies[@key] sid ||= request.params[@key] unless @cookie_only sid end # Returns the current session id from the SessionHash. def current_session_id(req) req.get_header(RACK_SESSION).id end # Check if the session exists or not. def session_exists?(req) value = current_session_id(req) value && !value.empty? end # Session should be committed if it was loaded, any of specific options like :renew, :drop # or :expire_after was given and the security permissions match. Skips if skip is given. def commit_session?(req, session, options) if options[:skip] false else has_session = loaded_session?(session) || forced_session_update?(session, options) has_session && security_matches?(req, options) end end def loaded_session?(session) !session.is_a?(session_class) || session.loaded? end def forced_session_update?(session, options) force_options?(options) && session && !session.empty? end def force_options?(options) options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any? end def security_matches?(request, options) return true unless options[:secure] request.ssl? || @assume_ssl == true end # Acquires the session from the environment and the session id from # the session options and passes them to #write_session. If successful # and the :defer option is not true, a cookie will be added to the # response with the session's id. def commit_session(req, res) session = req.get_header RACK_SESSION options = session.options if options[:drop] || options[:renew] session_id = delete_session(req, session.id || generate_sid, options) return unless session_id end return unless commit_session?(req, session, options) session.send(:load!) unless loaded_session?(session) session_id ||= session.id session_data = session.to_hash.delete_if { |k, v| v.nil? } if not data = write_session(req, session_id, session_data, options) req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.") elsif options[:defer] and not options[:renew] req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE else cookie = Hash.new cookie[:value] = cookie_value(data) cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after] cookie[:expires] = Time.now + options[:max_age] if options[:max_age] if @same_site.respond_to? :call cookie[:same_site] = @same_site.call(req, res) else cookie[:same_site] = @same_site end set_cookie(req, res, cookie.merge!(options)) end end public :commit_session def cookie_value(data) data end # Sets the cookie back to the client with session id. We skip the cookie # setting if the value didn't change (sid is the same) or expires was given. def set_cookie(request, response, cookie) if request.cookies[@key] != cookie[:value] || cookie[:expires] response.set_cookie(@key, cookie) end end # Allow subclasses to prepare_session for different Session classes def session_class SessionHash end # All thread safety and session retrieval procedures should occur here. # Should return [session_id, session]. # If nil is provided as the session id, generation of a new valid id # should occur within. def find_session(env, sid) raise '#find_session not implemented.' end # All thread safety and session storage procedures should occur here. # Must return the session id if the session was saved successfully, or # false if the session could not be saved. def write_session(req, sid, session, options) raise '#write_session not implemented.' end # All thread safety and session destroy procedures should occur here. # Should return a new session id or nil if options[:drop] def delete_session(req, sid, options) raise '#delete_session not implemented' end end class PersistedSecure < Persisted class SecureSessionHash < SessionHash def [](key) if key == "session_id" load_for_read! case id when SessionId id.public_id else id end else super end end end def generate_sid(*) public_id = super SessionId.new(public_id) end def extract_session_id(*) public_id = super public_id && SessionId.new(public_id) end private def session_class SecureSessionHash end def cookie_value(data) data.cookie_value end end class ID < Persisted def self.inherited(klass) k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID } unless k.instance_variable_defined?(:"@_rack_warned") warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE k.instance_variable_set(:"@_rack_warned", true) end super end # All thread safety and session retrieval procedures should occur here. # Should return [session_id, session]. # If nil is provided as the session id, generation of a new valid id # should occur within. def find_session(req, sid) get_session req.env, sid end # All thread safety and session storage procedures should occur here. # Must return the session id if the session was saved successfully, or # false if the session could not be saved. def write_session(req, sid, session, options) set_session req.env, sid, session, options end # All thread safety and session destroy procedures should occur here. # Should return a new session id or nil if options[:drop] def delete_session(req, sid, options) destroy_session req.env, sid, options end end end end end rack-session-2.1.1/lib/rack/session/constants.rb000066400000000000000000000006011500636520100216210ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. # Copyright, 2022, by Jeremy Evans. module Rack module Session RACK_SESSION = 'rack.session' RACK_SESSION_OPTIONS = 'rack.session.options' RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data' end end rack-session-2.1.1/lib/rack/session/cookie.rb000066400000000000000000000235001500636520100210610ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. # Copyright, 2022, by Jeremy Evans. # Copyright, 2022, by Jon Dufresne. require 'openssl' require 'zlib' require 'json' require 'base64' require 'delegate' require 'rack/constants' require 'rack/utils' require_relative 'abstract/id' require_relative 'encryptor' require_relative 'constants' module Rack module Session # Rack::Session::Cookie provides simple cookie based session management. # By default, the session is a Ruby Hash that is serialized and encoded as # a cookie set to :key (default: rack.session). # # This middleware accepts a :secrets option which enables encryption of # session cookies. This option should be one or more random "secret keys" # that are each at least 64 bytes in length. Multiple secret keys can be # supplied in an Array, which is useful when rotating secrets. # # Several options are also accepted that are passed to Rack::Session::Encryptor. # These options include: # * :serialize_json # Use JSON for message serialization instead of Marshal. This can be # viewed as a security enhancement. # * :gzip_over # For message data over this many bytes, compress it with the deflate # algorithm. # # Refer to Rack::Session::Encryptor for more details on these options. # # Prior to version TODO, the session hash was stored as base64 encoded # marshalled data. When a :secret option was supplied, the integrity of the # encoded data was protected with HMAC-SHA1. This functionality is still # supported using a set of a legacy options. # # Lastly, a :coder option is also accepted. When used, both encryption and # the legacy HMAC will be skipped. This option could create security issues # in your application! # # Example: # # use Rack::Session::Cookie, { # key: 'rack.session', # domain: 'foo.com', # path: '/', # expire_after: 2592000, # secrets: 'a randomly generated, raw binary string 64 bytes in size', # } # # Example using legacy HMAC options: # # Rack::Session:Cookie.new(application, { # # The secret used for legacy HMAC cookies, this enables the functionality # legacy_hmac_secret: 'legacy secret', # # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal # legacy_hmac_coder: Rack::Session::Cookie::Identity.new, # # legacy_hmac will default to OpenSSL::Digest::SHA1 # legacy_hmac: OpenSSL::Digest::SHA256 # }) # # Example of a cookie with no encoding: # # Rack::Session::Cookie.new(application, { # :coder => Rack::Session::Cookie::Identity.new # }) # # Example of a cookie with custom encoding: # # Rack::Session::Cookie.new(application, { # :coder => Class.new { # def encode(str); str.reverse; end # def decode(str); str.reverse; end # }.new # }) # class Cookie < Abstract::PersistedSecure # Encode session cookies as Base64 class Base64 def encode(str) ::Base64.strict_encode64(str) end def decode(str) ::Base64.decode64(str) end # Encode session cookies as Marshaled Base64 data class Marshal < Base64 def encode(str) super(::Marshal.dump(str)) end def decode(str) return unless str ::Marshal.load(super(str)) rescue nil end end # N.B. Unlike other encoding methods, the contained objects must be a # valid JSON composite type, either a Hash or an Array. class JSON < Base64 def encode(obj) super(::JSON.dump(obj)) end def decode(str) return unless str ::JSON.parse(super(str)) rescue nil end end class ZipJSON < Base64 def encode(obj) super(Zlib::Deflate.deflate(::JSON.dump(obj))) end def decode(str) return unless str ::JSON.parse(Zlib::Inflate.inflate(super(str))) rescue nil end end end # Use no encoding for session cookies class Identity def encode(str); str; end def decode(str); str; end end class Marshal def encode(str) ::Marshal.dump(str) end def decode(str) ::Marshal.load(str) if str end end attr_reader :coder, :encryptors def initialize(app, options = {}) # support both :secrets and :secret for backwards compatibility secrets = [*(options[:secrets] || options[:secret])] encryptor_opts = { purpose: options[:key], serialize_json: options[:serialize_json] } # For each secret, create an Encryptor. We have iterate this Array at # decryption time to achieve key rotation. @encryptors = secrets.map do |secret| Rack::Session::Encryptor.new secret, encryptor_opts end # If a legacy HMAC secret is present, initialize those features. # Fallback to :secret for backwards compatibility. if options.has_key?(:legacy_hmac_secret) || options.has_key?(:secret) @legacy_hmac = options.fetch(:legacy_hmac, 'SHA1') @legacy_hmac_secret = options[:legacy_hmac_secret] || options[:secret] @legacy_hmac_coder = options.fetch(:legacy_hmac_coder, Base64::Marshal.new) else @legacy_hmac = false end warn <<-MSG unless secure?(options) SECURITY WARNING: No secret option provided to Rack::Session::Cookie. This poses a security threat. It is strongly recommended that you provide a secret to prevent exploits that may be possible from crafted cookies. This will not be supported in future versions of Rack, and future versions will even invalidate your existing user cookies. Called from: #{caller[0]}. MSG # Potential danger ahead! Marshal without verification and/or # encryption could present a major security issue. @coder = options[:coder] ||= Base64::Marshal.new super(app, options.merge!(cookie_only: true)) end private def find_session(req, sid) data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data["session_id"], data] end def extract_session_id(request) unpacked_cookie_data(request)&.[]("session_id") end def unpacked_cookie_data(request) request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k| if cookie_data = request.cookies[@key] session_data = nil # Try to decrypt the session data with our encryptors encryptors.each do |encryptor| begin session_data = encryptor.decrypt(cookie_data) break rescue Rack::Session::Encryptor::Error => error request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}" next end end # If session decryption fails but there is @legacy_hmac_secret # defined, attempt legacy HMAC verification if !session_data && @legacy_hmac_secret # Parse and verify legacy HMAC session cookie session_data, _, digest = cookie_data.rpartition('--') session_data = nil unless legacy_digest_match?(session_data, digest) # Decode using legacy HMAC decoder session_data = @legacy_hmac_coder.decode(session_data) elsif !session_data && coder # Use the coder option, which has the potential to be very unsafe session_data = coder.decode(cookie_data) end end request.set_header(k, session_data || {}) end end def persistent_session_id!(data, sid = nil) data ||= {} data["session_id"] ||= sid || generate_sid data end class SessionId < DelegateClass(Session::SessionId) attr_reader :cookie_value def initialize(session_id, cookie_value) super(session_id) @cookie_value = cookie_value end end def write_session(req, session_id, session, options) session = session.merge("session_id" => session_id) session_data = encode_session_data(session) if session_data.size > (4096 - @key.size) req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.") nil else SessionId.new(session_id, session_data) end end def delete_session(req, session_id, options) # Nothing to do here, data is in the client generate_sid unless options[:drop] end def legacy_digest_match?(data, digest) return false unless data && digest Rack::Utils.secure_compare(digest, legacy_generate_hmac(data)) end def legacy_generate_hmac(data) OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data) end def encode_session_data(session) if encryptors.empty? coder.encode(session) else encryptors.first.encrypt(session) end end # Were consider "secure" if: # * Encrypted cookies are enabled and one or more encryptor is # initialized # * The legacy HMAC option is enabled # * Customer :coder is used, with :let_coder_handle_secure_encoding # set to true def secure?(options) !@encryptors.empty? || @legacy_hmac || (options[:coder] && options[:let_coder_handle_secure_encoding]) end end end end rack-session-2.1.1/lib/rack/session/encryptor.rb000066400000000000000000000135201500636520100216360ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. # Copyright, 2022, by Philip Arndt. require 'base64' require 'openssl' require 'securerandom' require 'zlib' require 'rack/utils' module Rack module Session class Encryptor class Error < StandardError end class InvalidSignature < Error end class InvalidMessage < Error end # The secret String must be at least 64 bytes in size. The first 32 bytes # will be used for the encryption cipher key. The remainder will be used # for an HMAC key. # # Options may include: # * :serialize_json # Use JSON for message serialization instead of Marshal. This can be # viewed as a security enhancement. # * :pad_size # Pad encrypted message data, to a multiple of this many bytes # (default: 32). This can be between 2-4096 bytes, or +nil+ to disable # padding. # * :purpose # Limit messages to a specific purpose. This can be viewed as a # security enhancement to prevent message reuse from different contexts # if keys are reused. # # Cryptography and Output Format: # # urlsafe_encode64(version + random_data + IV + encrypted data + HMAC) # # Where: # * version - 1 byte and is currently always 0x01 # * random_data - 32 bytes used for generating the per-message secret # * IV - 16 bytes random initialization vector # * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose # value def initialize(secret, opts = {}) raise ArgumentError, "secret must be a String" unless String === secret raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64 case opts[:pad_size] when nil # padding is disabled when Integer raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size] else raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil" end @options = { serialize_json: false, pad_size: 32, purpose: nil }.update(opts) @hmac_secret = secret.dup.force_encoding('BINARY') @cipher_secret = @hmac_secret.slice!(0, 32) @hmac_secret.freeze @cipher_secret.freeze end def decrypt(base64_data) data = Base64.urlsafe_decode64(base64_data) signature = data.slice!(-32..-1) verify_authenticity! data, signature # The version is reserved for future _version = data.slice!(0, 1) message_secret = data.slice!(0, 32) cipher_iv = data.slice!(0, 16) cipher = new_cipher cipher.decrypt set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret)) cipher.iv = cipher_iv data = cipher.update(data) << cipher.final deserialized_message data rescue ArgumentError raise InvalidSignature, 'Message invalid' end def encrypt(message) version = "\1" serialized_payload = serialize_payload(message) message_secret, cipher_secret = new_message_and_cipher_secret cipher = new_cipher cipher.encrypt set_cipher_key(cipher, cipher_secret) cipher_iv = cipher.random_iv encrypted_data = cipher.update(serialized_payload) << cipher.final data = String.new data << version data << message_secret data << cipher_iv data << encrypted_data data << compute_signature(data) Base64.urlsafe_encode64(data) end private def new_cipher OpenSSL::Cipher.new('aes-256-ctr') end def new_message_and_cipher_secret message_secret = SecureRandom.random_bytes(32) [message_secret, cipher_secret_from_message_secret(message_secret)] end def cipher_secret_from_message_secret(message_secret) OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret, message_secret) end def set_cipher_key(cipher, key) cipher.key = key end def serializer @serializer ||= @options[:serialize_json] ? JSON : Marshal end def compute_signature(data) signing_data = data signing_data += @options[:purpose] if @options[:purpose] OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_secret, signing_data) end def verify_authenticity!(data, signature) raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil? unless Rack::Utils.secure_compare(signature, compute_signature(data)) raise InvalidSignature, 'HMAC is invalid' end end # Returns a serialized payload of the message. If a :pad_size is supplied, # the message will be padded. The first 2 bytes of the returned string will # indicating the amount of padding. def serialize_payload(message) serialized_data = serializer.dump(message) return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil? padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size] padding_data = SecureRandom.random_bytes(padding_bytes) "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}" end # Return the deserialized message. The first 2 bytes will be read as the # amount of padding. def deserialized_message(data) # Read the first 2 bytes as the padding_bytes size padding_bytes, = data.unpack('v') # Slice out the serialized_data and deserialize it serialized_data = data.slice(2 + padding_bytes, data.bytesize) serializer.load serialized_data end end end end rack-session-2.1.1/lib/rack/session/pool.rb000066400000000000000000000046231500636520100205660ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. require_relative 'abstract/id' module Rack module Session # Rack::Session::Pool provides simple cookie based session management. # Session data is stored in a hash held by @pool. # In the context of a multithreaded environment, sessions being # committed to the pool is done in a merging manner. # # The :drop option is available in rack.session.options if you wish to # explicitly remove the session from the session cache. # # Example: # myapp = MyRackApp.new # sessioned = Rack::Session::Pool.new(myapp, # :domain => 'foo.com', # :expire_after => 2592000 # ) # Rack::Handler::WEBrick.run sessioned class Pool < Abstract::PersistedSecure attr_reader :mutex, :pool DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge(drop: false, allow_fallback: true) def initialize(app, options = {}) super @pool = Hash.new @mutex = Mutex.new @allow_fallback = @default_options.delete(:allow_fallback) end def generate_sid(*args, use_mutex: true) loop do sid = super(*args) break sid unless use_mutex ? @mutex.synchronize { @pool.key? sid.private_id } : @pool.key?(sid.private_id) end end def find_session(req, sid) @mutex.synchronize do unless sid and session = get_session_with_fallback(sid) sid, session = generate_sid(use_mutex: false), {} @pool.store sid.private_id, session end [sid, session] end end def write_session(req, session_id, new_session, options) @mutex.synchronize do return false unless get_session_with_fallback(session_id) @pool.store session_id.private_id, new_session session_id end end def delete_session(req, session_id, options) @mutex.synchronize do @pool.delete(session_id.public_id) @pool.delete(session_id.private_id) unless options[:drop] sid = generate_sid(use_mutex: false) @pool.store(sid.private_id, {}) sid end end end private def get_session_with_fallback(sid) @pool[sid.private_id] || (@pool[sid.public_id] if @allow_fallback) end end end end rack-session-2.1.1/lib/rack/session/version.rb000066400000000000000000000002531500636520100212750ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. module Rack module Session VERSION = "2.1.1" end end rack-session-2.1.1/license.md000066400000000000000000000057511500636520100160660ustar00rootroot00000000000000# MIT License Copyright, 2007-2008, by Leah Neukirchen. Copyright, 2007-2009, by Scytrin dai Kinthra. Copyright, 2008, by Daniel Roethlisberger. Copyright, 2009, by Joshua Peek. Copyright, 2009, by Mickaël Riga. Copyright, 2010, by Simon Chiang. Copyright, 2010-2011, by José Valim. Copyright, 2010-2013, by James Tucker. Copyright, 2010-2019, by Aaron Patterson. Copyright, 2011, by Max Cantor. Copyright, 2011-2012, by Konstantin Haase. Copyright, 2011, by Will Leinweber. Copyright, 2011, by John Manoogian III. Copyright, 2012, by Yun Huang Yong. Copyright, 2012, by Ravil Bayramgalin. Copyright, 2012, by Timothy Elliott. Copyright, 2012, by Jamie Macey. Copyright, 2012-2015, by Santiago Pastorino. Copyright, 2013, by Andrew Cole. Copyright, 2013, by Postmodern. Copyright, 2013, by Vipul A M. Copyright, 2013, by Charles Hornberger. Copyright, 2014, by Michal Bryxí. Copyright, 2015, by deepj. Copyright, 2015, by Doug McInnes. Copyright, 2015, by David Runger. Copyright, 2015, by Francesco Rodríguez. Copyright, 2015, by Yuichiro Kaneko. Copyright, 2015, by Michael Sauter. Copyright, 2016, by Kir Shatrov. Copyright, 2016, by Yann Vanhalewyn. Copyright, 2016, by Jian Weihang. Copyright, 2017, by Jordan Raine. Copyright, 2018, by Dillon Welch. Copyright, 2018, by Yoshiyuki Hirano. Copyright, 2019, by Krzysztof Rybka. Copyright, 2019, by Frederick Cheung. Copyright, 2019, by Adrian Setyadi. Copyright, 2019, by Rafael Mendonça França. Copyright, 2019-2020, by Pavel Rosicky. Copyright, 2019, by Dima Fatko. Copyright, 2019, by Oleh Demianiuk. Copyright, 2020-2023, by Samuel Williams. Copyright, 2020-2022, by Jeremy Evans. Copyright, 2020, by Alex Speller. Copyright, 2020, by Ryuta Kamizono. Copyright, 2020, by Yudai Suzuki. Copyright, 2020, by Bart de Water. Copyright, 2020, by Alec Clarke. Copyright, 2021, by Michael Coyne. Copyright, 2022, by Philip Arndt. Copyright, 2022, by Jon Dufresne. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rack-session-2.1.1/rack-session.gemspec000066400000000000000000000015741500636520100200670ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/rack/session/version" Gem::Specification.new do |spec| spec.name = "rack-session" spec.version = Rack::Session::VERSION spec.summary = "A session implementation for Rack." spec.authors = ["Samuel Williams", "Jeremy Evans", "Jon Dufresne", "Philip Arndt"] spec.license = "MIT" spec.homepage = "https://github.com/rack/rack-session" spec.files = Dir['{lib}/**/*', '*.md'] spec.required_ruby_version = ">= 2.5" spec.metadata = { "rubygems_mfa_required" => "true" } spec.add_dependency "base64", ">= 0.1.0" spec.add_dependency "rack", ">= 3.0.0" spec.add_development_dependency "bundler" spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "minitest-global_expectations" spec.add_development_dependency "minitest-sprint" spec.add_development_dependency "rake" end rack-session-2.1.1/readme.md000066400000000000000000000023561500636520100156770ustar00rootroot00000000000000# Rack::Session Session management implementation for Rack. [![Development Status](https://github.com/rack/rack-session/workflows/Test/badge.svg)](https://github.com/rack/rack-session/actions?workflow=Test) ## Usage In your `config.ru`: ``` ruby # config.ru require 'rack/session' use Rack::Session::Cookie, :domain => 'mywebsite.com', :path => '/', :expire_after => 3600*24, :secret => '**unique secret key**' ``` Usage follows the standard outlined by `rack.session`, i.e.: ``` ruby class MyApp def call(env) session = env['rack.session'] # Set some state: session[:key] = "value" end end ``` ### Compatibility `rack-session` code used to be part of Rack, but it was extracted in Rack v3 to this gem. The v1 release of this gem is compatible with Rack v2, and the v2 release of this gem is compatible with Rack v3+. That means you can add `gem "rack-session"` to your application and it will be compatible with all versions of Rack. ## Contributing We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). 3. Commit your changes (`git commit -am 'Add some feature'`). 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. rack-session-2.1.1/releases.md000066400000000000000000000011311500636520100162330ustar00rootroot00000000000000# Releases ## v2.1.1 - Prevent `Rack::Session::Pool` from recreating deleted sessions [CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj). ## v2.1.0 - Improved compatibility with Ruby 3.3+ and Rack 3+. - Add support for cookie option `partitioned`. - Introduce `assume_ssl` option to allow secure session cookies through insecure proxy. ## v2.0.0 - Initial migration of code from Rack 2, for Rack 3 release. ## v1.0.2 - Fix missing `rack/session.rb` file. ## v1.0.1 - Pin to `rack < 3`. ## v1.0.0 - Empty shim release for Rack 2. rack-session-2.1.1/security.md000066400000000000000000000001451500636520100163030ustar00rootroot00000000000000# Security Policy Please see our main security policy: https://github.com/rack/rack/security/policy rack-session-2.1.1/test/000077500000000000000000000000001500636520100150715ustar00rootroot00000000000000rack-session-2.1.1/test/helper.rb000066400000000000000000000012551500636520100167000ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. if ENV.delete('COVERAGE') require 'coverage' require 'simplecov' def SimpleCov.rack_coverage(**opts) start do add_filter "/test/" add_group('Missing'){|src| src.covered_percent < 100} add_group('Covered'){|src| src.covered_percent == 100} end end SimpleCov.rack_coverage end $:.unshift(File.expand_path('../lib', __dir__)) if ENV['SEPARATE'] def self.separate_testing yield end else require_relative '../lib/rack/session' def self.separate_testing end end require 'minitest/global_expectations/autorun' require 'stringio' rack-session-2.1.1/test/spec_session_abstract_id.rb000066400000000000000000000043771500636520100224650ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. require_relative 'helper' require 'rack/request' ### WARNING: there be hax in this file. require_relative '../lib/rack/session/abstract/id' describe Rack::Session::Abstract::ID do attr_reader :id def setup super @id = Rack::Session::Abstract::ID end it "use securerandom" do assert_equal ::SecureRandom, id::DEFAULT_OPTIONS[:secure_random] id = @id.new nil assert_equal ::SecureRandom, id.sid_secure end it "allow to use another securerandom provider" do secure_random = Class.new do def hex(*args) 'fake_hex' end end id = Rack::Session::Abstract::ID.new nil, secure_random: secure_random.new id.send(:generate_sid).must_equal 'fake_hex' end it "should warn when subclassing" do verbose = $VERBOSE begin $VERBOSE = true warn_arg = nil @id.define_singleton_method(:warn) do |arg| warn_arg = arg end c = Class.new(@id) regexp = /is inheriting from Rack::Session::Abstract::ID. Inheriting from Rack::Session::Abstract::ID is deprecated, please inherit from Rack::Session::Abstract::Persisted instead/ warn_arg.must_match(regexp) warn_arg = nil c = Class.new(c) warn_arg.must_be_nil ensure $VERBOSE = verbose @id.singleton_class.send(:remove_method, :warn) end end it "#find_session should find session in request" do id = @id.new(nil) def id.get_session(env, sid) [env['rack.session'], generate_sid] end req = Rack::Request.new('rack.session' => {}) session, sid = id.find_session(req, nil) session.must_equal({}) sid.must_match(/\A\h+\z/) end it "#write_session should write session to request" do id = @id.new(nil) def id.set_session(env, sid, session, options) [env, sid, session, options] end req = Rack::Request.new({}) id.write_session(req, 1, 2, 3).must_equal [{}, 1, 2, 3] end it "#delete_session should remove session from request" do id = @id.new(nil) def id.destroy_session(env, sid, options) [env, sid, options] end req = Rack::Request.new({}) id.delete_session(req, 1, 2).must_equal [{}, 1, 2] end end rack-session-2.1.1/test/spec_session_abstract_persisted.rb000066400000000000000000000057141500636520100240670ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. require_relative 'helper' require 'rack/request' require_relative '../lib/rack/session/abstract/id' describe Rack::Session::Abstract::Persisted do def setup @class = Rack::Session::Abstract::Persisted @pers = @class.new(nil) end it "#generated_sid generates a session identifier" do @pers.send(:generate_sid).must_match(/\A\h+\z/) @pers.send(:generate_sid, nil).must_match(/\A\h+\z/) obj = Object.new def obj.hex(_); raise NotImplementedError end @pers.send(:generate_sid, obj).must_match(/\A\h+\z/) end it "#commit_session? returns false if :skip option is given" do @pers.send(:commit_session?, Rack::Request.new({}), {}, skip: true).must_equal false end it "#commit_session writes to rack.errors if session cannot be written" do @pers = @class.new(nil) def @pers.write_session(*) end errors = StringIO.new env = { 'rack.errors' => errors } req = Rack::Request.new(env) store = Class.new do def load_session(req) ["id", {}] end def session_exists?(req) true end end session = env['rack.session'] = Rack::Session::Abstract::SessionHash.new(store.new, req) session['foo'] = 'bar' @pers.send(:commit_session, req, Rack::Response.new) errors.rewind errors.read.must_equal "Warning! Rack::Session::Abstract::Persisted failed to save session. Content dropped.\n" end it "#cookie_value returns its argument" do obj = Object.new @pers.send(:cookie_value, obj).must_equal(obj) end it "#session_class returns the default session class" do @pers.send(:session_class).must_equal Rack::Session::Abstract::SessionHash end it "#find_session raises" do proc { @pers.send(:find_session, nil, nil) }.must_raise RuntimeError end it "#write_session raises" do proc { @pers.send(:write_session, nil, nil, nil, nil) }.must_raise RuntimeError end it "#delete_session raises" do proc { @pers.send(:delete_session, nil, nil, nil) }.must_raise RuntimeError end describe '#security_matches?' do it '#security_matches? returns true if secure cookie is off' do @pers.send(:security_matches?, Rack::Request.new({}), {}).must_equal true end it '#security_matches? returns true if ssl is on' do req = Rack::Request.new({}) req.set_header('HTTPS', 'on') @pers.send(:security_matches?, req, { secure: true }).must_equal true end it '#security_matches? returns true if assume_ssl option is set' do req = Rack::Request.new({}) pers_with_persist = @class.new(nil, { assume_ssl: true }) pers_with_persist.send(:security_matches?, req, { secure: true }).must_equal true end it '#security_matches? returns false if secure cookie is on, but not ssl or assume_ssl' do @pers.send(:security_matches?, Rack::Request.new({}), { secure: true }).must_equal false end end end rack-session-2.1.1/test/spec_session_abstract_persisted_secure_secure_session_hash.rb000066400000000000000000000042751500636520100315520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. require_relative 'helper' require 'rack/request' require_relative '../lib/rack/session/abstract/id' describe Rack::Session::Abstract::PersistedSecure::SecureSessionHash do attr_reader :hash def setup super @store = Class.new do def load_session(req) [Rack::Session::SessionId.new("id"), { foo: :bar, baz: :qux }] end def session_exists?(req) true end end @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(@store.new, nil) end it "returns keys" do assert_equal ["foo", "baz"], hash.keys end it "returns values" do assert_equal [:bar, :qux], hash.values end describe "#[]" do it "returns value for a matching key" do assert_equal :bar, hash[:foo] end it "returns value for a 'session_id' key" do assert_equal "id", hash['session_id'] end it "returns nil value for missing 'session_id' key" do store = @store.new def store.load_session(req) [nil, {}] end @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(store, nil) assert_nil hash['session_id'] end it "returns value for non SessionId 'session_id' key" do store = @store.new def store.load_session(req) ["id", {}] end @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(store, nil) assert_equal "id", hash['session_id'] end end describe "#fetch" do it "returns value for a matching key" do assert_equal :bar, hash.fetch(:foo) end it "works with a default value" do assert_equal :default, hash.fetch(:unknown, :default) end it "works with a block" do assert_equal :default, hash.fetch(:unknown) { :default } end it "it raises when fetching unknown keys without defaults" do lambda { hash.fetch(:unknown) }.must_raise KeyError end end describe "#stringify_keys" do it "returns hash or session hash with keys stringified" do assert_equal({ "foo" => :bar, "baz" => :qux }, hash.send(:stringify_keys, hash).to_h) end end end rack-session-2.1.1/test/spec_session_abstract_session_hash.rb000066400000000000000000000052541500636520100245520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. require_relative 'helper' require 'rack/request' require_relative '../lib/rack/session/abstract/id' describe Rack::Session::Abstract::SessionHash do attr_reader :hash def setup super store = Class.new do def load_session(req) ["id", { foo: :bar, baz: :qux, x: { y: 1 } }] end def session_exists?(req) true end end @class = Rack::Session::Abstract::SessionHash @hash = @class.new(store.new, nil) end it ".find finds entry in request" do assert_equal({}, @class.find(Rack::Request.new('rack.session' => {}))) end it ".set sets session in request" do req = Rack::Request.new({}) @class.set(req, {}) req.env['rack.session'].must_equal({}) end it ".set_options sets session options in request" do req = Rack::Request.new({}) h = {} @class.set_options(req, h) opts = req.env['rack.session.options'] opts.must_equal(h) opts.wont_be_same_as(h) end it "#keys returns keys" do assert_equal ["foo", "baz", "x"], hash.keys end it "#values returns values" do assert_equal [:bar, :qux, { y: 1 }], hash.values end it "#dig operates like Hash#dig" do assert_equal({ y: 1 }, hash.dig("x")) assert_equal(1, hash.dig(:x, :y)) assert_nil(hash.dig(:z)) assert_nil(hash.dig(:x, :z)) lambda { hash.dig(:x, :y, :z) }.must_raise TypeError lambda { hash.dig }.must_raise ArgumentError end it "#each iterates over entries" do a = [] @hash.each do |k, v| a << [k, v] end a.must_equal [["foo", :bar], ["baz", :qux], ["x", { y: 1 }]] end it "#has_key returns whether the key is in the hash" do assert_equal true, hash.has_key?("foo") assert_equal true, hash.has_key?(:foo) assert_equal false, hash.has_key?("food") assert_equal false, hash.has_key?(:food) end it "#replace replaces hash" do hash.replace({ bar: "foo" }) assert_equal "foo", hash["bar"] end describe "#fetch" do it "returns value for a matching key" do assert_equal :bar, hash.fetch(:foo) end it "works with a default value" do assert_equal :default, hash.fetch(:unknown, :default) end it "works with a block" do assert_equal :default, hash.fetch(:unknown) { :default } end it "it raises when fetching unknown keys without defaults" do lambda { hash.fetch(:unknown) }.must_raise KeyError end end it "#stringify_keys returns hash or session hash with keys stringified" do assert_equal({ "foo" => :bar, "baz" => :qux, "x" => { y: 1 } }, hash.send(:stringify_keys, hash).to_h) end end rack-session-2.1.1/test/spec_session_cookie.rb000066400000000000000000000461011500636520100214460ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. # Copyright, 2022, by Jeremy Evans. require_relative 'helper' require 'rack/response' require 'rack/lint' require 'rack/mock' require 'json' require_relative '../lib/rack/session/cookie' describe Rack::Session::Cookie do incrementor = lambda do |env| env["rack.session"]["counter"] ||= 0 env["rack.session"]["counter"] += 1 hash = env["rack.session"].dup hash.delete("session_id") Rack::Response.new(hash.inspect).to_a end session_id = lambda do |env| Rack::Response.new(env["rack.session"].to_hash.inspect).to_a end session_option = lambda do |opt| lambda do |env| Rack::Response.new(env["rack.session.options"][opt].inspect).to_a end end nothing = lambda do |env| Rack::Response.new("Nothing").to_a end renewer = lambda do |env| env["rack.session.options"][:renew] = true Rack::Response.new("Nothing").to_a end only_session_id = lambda do |env| Rack::Response.new(env["rack.session"]["session_id"].to_s).to_a end bigcookie = lambda do |env| env["rack.session"]["cookie"] = "big" * 3000 Rack::Response.new(env["rack.session"].inspect).to_a end destroy_session = lambda do |env| env["rack.session"].destroy Rack::Response.new("Nothing").to_a end def response_for(options = {}) request_options = options.fetch(:request, {}) cookie = if options[:cookie].is_a?(Rack::Response) options[:cookie]["Set-Cookie"] else options[:cookie] end request_options["HTTP_COOKIE"] = cookie || "" app_with_cookie = Rack::Session::Cookie.new(*options[:app]) app_with_cookie = Rack::Lint.new(app_with_cookie) Rack::MockRequest.new(app_with_cookie).get("/", request_options) end def random_encryptor_secret SecureRandom.random_bytes(64) end before do # Random key, as a hex string @secret = random_encryptor_secret @warnings = warnings = [] Rack::Session::Cookie.class_eval do define_method(:warn) { |m| warnings << m } end end after do Rack::Session::Cookie.class_eval { remove_method :warn } end describe 'Base64' do it 'uses base64 to encode' do coder = Rack::Session::Cookie::Base64.new str = 'fuuuuu' coder.encode(str).must_equal [str].pack('m0') end it 'uses base64 to decode' do coder = Rack::Session::Cookie::Base64.new str = ['fuuuuu'].pack('m0') coder.decode(str).must_equal str.unpack('m0').first end it 'handles non-strict base64 encoding' do coder = Rack::Session::Cookie::Base64.new str = ['A' * 256].pack('m') coder.decode(str).must_equal 'A' * 256 end describe 'Marshal' do it 'marshals and base64 encodes' do coder = Rack::Session::Cookie::Base64::Marshal.new str = 'fuuuuu' coder.encode(str).must_equal [::Marshal.dump(str)].pack('m0') end it 'marshals and base64 decodes' do coder = Rack::Session::Cookie::Base64::Marshal.new str = [::Marshal.dump('fuuuuu')].pack('m0') coder.decode(str).must_equal ::Marshal.load(str.unpack('m0').first) end it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::Marshal.new coder.decode('lulz').must_be_nil end end describe 'JSON' do it 'JSON and base64 encodes' do coder = Rack::Session::Cookie::Base64::JSON.new obj = %w[fuuuuu] coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m0') end it 'JSON and base64 decodes' do coder = Rack::Session::Cookie::Base64::JSON.new str = [::JSON.dump(%w[fuuuuu])].pack('m0') coder.decode(str).must_equal ::JSON.parse(str.unpack('m0').first) end it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::JSON.new coder.decode('lulz').must_be_nil end end describe 'ZipJSON' do it 'jsons, deflates, and base64 encodes' do coder = Rack::Session::Cookie::Base64::ZipJSON.new obj = %w[fuuuuu] json = JSON.dump(obj) coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m0') end it 'base64 decodes, inflates, and decodes json' do coder = Rack::Session::Cookie::Base64::ZipJSON.new obj = %w[fuuuuu] json = JSON.dump(obj) b64 = [Zlib::Deflate.deflate(json)].pack('m0') coder.decode(b64).must_equal obj end it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::ZipJSON.new coder.decode('lulz').must_be_nil end end end it "warns if no secret is given" do Rack::Session::Cookie.new(incrementor) @warnings.first.must_match(/no secret/i) @warnings.clear Rack::Session::Cookie.new(incrementor, secrets: @secret) @warnings.must_be :empty? end it 'abort if secret is too short' do lambda { Rack::Session::Cookie.new(incrementor, secrets: @secret[0, 16]) }.must_raise ArgumentError end it "doesn't warn if coder is configured to handle encoding" do Rack::Session::Cookie.new( incrementor, coder: Object.new, let_coder_handle_secure_encoding: true) @warnings.must_be :empty? end it "still warns if coder is not set" do Rack::Session::Cookie.new( incrementor, let_coder_handle_secure_encoding: true) @warnings.first.must_match(/no secret/i) end it 'uses a coder' do identity = Class.new { attr_reader :calls def initialize @calls = [] end def encode(hash); @calls << :encode; hash.to_json; end def decode(str); @calls << :decode; JSON.parse(str); end }.new response = response_for(app: [incrementor, { coder: identity }]) response["Set-Cookie"].must_include "rack.session=" response.body.must_equal ({"counter"=>1}.to_s) identity.calls.must_equal [:encode] response = response_for(app: [incrementor, { coder: identity }], :cookie=>response["Set-Cookie"].split(';', 2).first) identity.calls.must_equal [:encode, :decode, :encode] end it "creates a new cookie" do response = response_for(app: incrementor) response["Set-Cookie"].must_include "rack.session=" response.body.must_equal ({"counter"=>1}.to_s) end it "passes through same_site option to session cookie" do response = response_for(app: [incrementor, same_site: :none]) response["Set-Cookie"].must_match /SameSite=None/i end it "allows using a lambda to specify same_site option, because some browsers require different settings" do # Details of why this might need to be set dynamically: # https://www.chromium.org/updates/same-site/incompatible-clients # https://gist.github.com/bnorton/7dee72023787f367c48b3f5c2d71540f response = response_for(app: [incrementor, same_site: lambda { |req, res| :none }]) response["Set-Cookie"].must_match /SameSite=None/i response = response_for(app: [incrementor, same_site: lambda { |req, res| :lax }]) response["Set-Cookie"].must_match /SameSite=Lax/i end it "loads from a cookie" do response = response_for(app: incrementor) response = response_for(app: incrementor, cookie: response) response.body.must_equal ({"counter"=>2}.to_s) response = response_for(app: incrementor, cookie: response) response.body.must_equal ({"counter"=>3}.to_s) end it "renew session id" do response = response_for(app: incrementor) cookie = response['Set-Cookie'] response = response_for(app: only_session_id, cookie: cookie) cookie = response['Set-Cookie'] if response['Set-Cookie'] response.body.wont_equal "" old_session_id = response.body response = response_for(app: renewer, cookie: cookie) cookie = response['Set-Cookie'] if response['Set-Cookie'] response = response_for(app: only_session_id, cookie: cookie) response.body.wont_equal "" response.body.wont_equal old_session_id end it "destroys session" do response = response_for(app: incrementor) response = response_for(app: only_session_id, cookie: response) response.body.wont_equal "" old_session_id = response.body response = response_for(app: destroy_session, cookie: response) response = response_for(app: only_session_id, cookie: response) response.body.wont_equal "" response.body.wont_equal old_session_id end it "survives broken cookies" do response = response_for( app: incrementor, cookie: "rack.session=blarghfasel" ) response.body.must_equal ({"counter"=>1}.to_s) response = response_for( app: [incrementor, { secrets: @secret }], cookie: "rack.session=" ) response.body.must_equal ({"counter"=>1}.to_s) end it "barks on too big cookies" do lambda{ response_for(app: bigcookie, request: { fatal: true }) }.must_raise Rack::MockRequest::FatalWarning end it "loads from a cookie with encryption" do app = [incrementor, { secrets: @secret }] response = response_for(app: app) response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>2}.to_s) response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>3}.to_s) app = [incrementor, { secrets: random_encryptor_secret }] response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>1}.to_s) end it "loads from a cookie with accept-only integrity hash for graceful key rotation" do response = response_for(app: [incrementor, { secrets: @secret }]) new_secret = random_encryptor_secret app = [incrementor, { secrets: [new_secret, @secret] }] response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>2}.to_s) newer_secret = random_encryptor_secret app = [incrementor, { secrets: [newer_secret, new_secret] }] response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>3}.to_s) end it 'loads from a legacy hmac cookie' do legacy_session = Rack::Session::Cookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' }) legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session) legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly" app = [incrementor, { secrets: @secret, legacy_hmac_secret: legacy_secret }] response = response_for(app: app, cookie: legacy_cookie) response.body.must_equal ({"counter"=>2}.to_s) end it "ignores tampered session cookies" do app = [incrementor, { secrets: @secret }] response = response_for(app: app) response.body.must_equal ({"counter"=>1}.to_s) response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>2}.to_s) encoded_cookie = response["Set-Cookie"].split('=', 2).last.split(';').first decoded_cookie = Base64.urlsafe_decode64(Rack::Utils.unescape(encoded_cookie)) tampered_cookie = "rack.session=#{Base64.urlsafe_encode64(decoded_cookie.tap { |m| m[m.size - 1] = (m[m.size - 1].unpack('C')[0] ^ 1).chr })}" response = response_for(app: app, cookie: tampered_cookie) response.body.must_equal ({"counter"=>1}.to_s) end it 'rejects session cookie with different purpose' do app = [incrementor, { secrets: @secrets }] other_app = [incrementor, { secrets: @secrets, key: 'other' }] response = response_for(app: app) response.body.must_equal ({"counter"=>1}.to_s) response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>2}.to_s) response = response_for(app: other_app, cookie: response) response.body.must_equal ({"counter"=>1}.to_s) end it 'adds to RACK_ERRORS on encryptor errors' do echo_rack_errors = lambda do |env| env["rack.session"]["counter"] ||= 0 env["rack.session"]["counter"] += 1 Rack::Response.new(env[Rack::RACK_ERRORS].flush.tap(&:rewind).read).to_a end app = [incrementor, { secrets: @secret }] err_app = [echo_rack_errors, { secrets: @secret }] response = response_for(app: app) response.body.must_equal ({"counter"=>1}.to_s) encoded_cookie = response["Set-Cookie"].split('=', 2).last.split(';').first decoded_cookie = Base64.urlsafe_decode64(Rack::Utils.unescape(encoded_cookie)) tampered_cookie = "rack.session=#{Base64.urlsafe_encode64(decoded_cookie.tap { |m| m[m.size - 1] = "\0" })}" response = response_for(app: err_app, cookie: tampered_cookie) response.body.must_equal "Session cookie encryptor error: HMAC is invalid\n" end it 'ignores tampered with legacy hmac cookie' do legacy_session = Rack::Session::Cookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' }) legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session).reverse legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly" app = [incrementor, { secret: @secret, legacy_hmac_secret: legacy_secret }] response = response_for(app: app, cookie: legacy_cookie) response.body.must_equal ({"counter"=>1}.to_s) end it "supports custom digest instance for legacy hmac cookie" do legacy_hmac = 'SHA256' legacy_session = Rack::Session::Cookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' }) legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(legacy_hmac, legacy_secret, legacy_session) legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly" app = [incrementor, { secrets: @secret, legacy_hmac_secret: legacy_secret, legacy_hmac: legacy_hmac }] response = response_for(app: app, cookie: legacy_cookie) response.body.must_equal ({"counter"=>2}.to_s) response = response_for(app: app, cookie: response) response.body.must_equal ({"counter"=>3}.to_s) end it "can handle Rack::Lint middleware" do response = response_for(app: incrementor) lint = Rack::Lint.new(session_id) response = response_for(app: lint, cookie: response) response.body.wont_be :nil? end it "can handle middleware that inspects the env" do class TestEnvInspector def initialize(app) @app = app end def call(env) env.inspect @app.call(env) end end response = response_for(app: incrementor) inspector = TestEnvInspector.new(session_id) response = response_for(app: inspector, cookie: response) response.body.wont_be :nil? end it "returns the session id in the session hash" do response = response_for(app: incrementor) response.body.must_equal ({"counter"=>1}.to_s) response = response_for(app: session_id, cookie: response) response.body.must_match(/"session_id"\s*=>/) response.body.must_match(/"counter"\s*=>\s*1/) end it "does not return a cookie if set to secure but not using ssl" do app = [incrementor, { secure: true }] response = response_for(app: app) response["Set-Cookie"].must_be_nil response = response_for(app: app, request: { "HTTPS" => "on" }) response["Set-Cookie"].wont_be :nil? response["Set-Cookie"].must_match(/secure/) end it "does not return a cookie if cookie was not read/written" do response = response_for(app: nothing) response["Set-Cookie"].must_be_nil end it "does not return a cookie if cookie was not written (only read)" do response = response_for(app: session_id) response["Set-Cookie"].must_be_nil end it "returns even if not read/written if :expire_after is set" do app = [nothing, { expire_after: 3600 }] request = { "rack.session" => { "not" => "empty" } } response = response_for(app: app, request: request) response["Set-Cookie"].wont_be :nil? end it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do app = [nothing, { expire_after: 3600 }] response = response_for(app: app) response["Set-Cookie"].must_be_nil end it "exposes :secrets in env['rack.session.option']" do response = response_for(app: [session_option[:secrets], { secrets: @secret }]) response.body.must_equal @secret.inspect end it "exposes :coder in env['rack.session.option']" do response = response_for(app: session_option[:coder]) response.body.must_match(/Base64::Marshal/) end it 'exposes correct :coder when a secrets is used' do response = response_for(app: session_option[:coder], secrets: @secret) response.body.must_match(/Marshal/) end it "allows passing in a hash with session data from middleware in front" do request = { 'rack.session' => { foo: 'bar' } } response = response_for(app: session_id, request: request) response.body.must_match(/foo/) end it "allows modifying session data with session data from middleware in front" do request = { 'rack.session' => { foo: 'bar' } } response = response_for(app: incrementor, request: request) response.body.must_match(/counter/) response.body.must_match(/foo/) end it "allows more than one '--' in the cookie when calculating legacy digests" do @counter = 0 app = lambda do |env| env["rack.session"]["message"] ||= "" env["rack.session"]["message"] += "#{(@counter += 1).to_s}--" hash = env["rack.session"].dup hash.delete("session_id") Rack::Response.new(hash["message"]).to_a end # another example of an unsafe coder is Base64.urlsafe_encode64 unsafe_coder = Class.new { def encode(hash); hash.inspect end def decode(str); eval(str) if str; end }.new legacy_session = unsafe_coder.encode('message' => "#{@counter += 1}--#{@counter += 1}--", 'session_id' => 'abcdef') legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session) legacy_cookie = "rack.session=#{Rack::Utils.escape legacy_session}--#{legacy_digest}; path=/; HttpOnly" _app = [ app, { secrets: @secret, legacy_hmac_secret: legacy_secret, legacy_hmac_coder: unsafe_coder }] response = response_for(app: _app, cookie: legacy_cookie) response.body.must_equal "1--2--3--" end it 'allows for non-strict encoded cookie' do long_session_app = lambda do |env| env['rack.session']['value'] = 'A' * 256 env['rack.session']['counter'] = 1 hash = env["rack.session"].dup hash.delete("session_id") Rack::Response.new(hash.inspect).to_a end non_strict_coder = Class.new { def encode(str) [Marshal.dump(str)].pack('m') end def decode(str) return unless str Marshal.load(str.unpack('m').first) end }.new non_strict_response = response_for(app: [ long_session_app, { coder: non_strict_coder } ]) response = response_for(app: [ incrementor ], cookie: non_strict_response) response.body.must_match /"value"\s*=>\s*"#{'A' * 256}"/ response.body.must_match /"counter"\s*=>\s*2/ response.body.must_match(/\A{[^}]+}\z/) end end rack-session-2.1.1/test/spec_session_encryptor.rb000066400000000000000000000130721500636520100222230ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. require_relative 'helper' require 'rack/session/encryptor' describe Rack::Session::Encryptor do def setup @secret = SecureRandom.random_bytes(64) end it 'initialize does not destroy key string' do encryptor = Rack::Session::Encryptor.new(@secret) @secret.size.must_equal 64 end it 'initialize raises ArgumentError on invalid key' do lambda { Rack::Session::Encryptor.new [:foo] }.must_raise ArgumentError end it 'initialize raises ArgumentError on short key' do lambda { Rack::Session::Encryptor.new 'key' }.must_raise ArgumentError end it 'decrypts an encrypted message' do encryptor = Rack::Session::Encryptor.new(@secret) message = encryptor.encrypt(foo: 'bar') encryptor.decrypt(message).must_equal foo: 'bar' end it 'decrypt raises InvalidSignature for tampered messages' do encryptor = Rack::Session::Encryptor.new(@secret) message = encryptor.encrypt(foo: 'bar') decoded_message = Base64.urlsafe_decode64(message) tampered_message = Base64.urlsafe_encode64(decoded_message.tap { |m| m[m.size - 1] = (m[m.size - 1].unpack('C')[0] ^ 1).chr }) lambda { encryptor.decrypt(tampered_message) }.must_raise Rack::Session::Encryptor::InvalidSignature end it 'decrypts an encrypted message with purpose' do encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing') message = encryptor.encrypt(foo: 'bar') encryptor.decrypt(message).must_equal foo: 'bar' end it 'decrypts raises InvalidSignature without purpose' do encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing') other_encryptor = Rack::Session::Encryptor.new(@secret) message = other_encryptor.encrypt(foo: 'bar') lambda { encryptor.decrypt(message) }.must_raise Rack::Session::Encryptor::InvalidSignature end it 'decrypts raises InvalidSignature with different purpose' do encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing') other_encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'other') message = other_encryptor.encrypt(foo: 'bar') lambda { encryptor.decrypt(message) }.must_raise Rack::Session::Encryptor::InvalidSignature end it 'initialize raises ArgumentError on invalid pad_size' do lambda { Rack::Session::Encryptor.new @secret, pad_size: :bar }.must_raise ArgumentError end it 'initialize raises ArgumentError on to short pad_size' do lambda { Rack::Session::Encryptor.new @secret, pad_size: 1 }.must_raise ArgumentError end it 'initialize raises ArgumentError on to long pad_size' do lambda { Rack::Session::Encryptor.new @secret, pad_size: 8023 }.must_raise ArgumentError end it 'decrypts an encrypted message without pad_size' do encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: nil) message = encryptor.encrypt(foo: 'bar') encryptor.decrypt(message).must_equal foo: 'bar' end it 'encryptor with pad_size increases message size' do no_pad_encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: nil) pad_encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: 64) message_without = Base64.urlsafe_decode64(no_pad_encryptor.encrypt('')) message_with = Base64.urlsafe_decode64(pad_encryptor.encrypt('')) message_size_diff = message_with.bytesize - message_without.bytesize message_with.bytesize.must_be :>, message_without.bytesize message_size_diff.must_equal 64 - Marshal.dump('').bytesize - 2 end it 'encryptor with pad_size has message payload size to multiple of pad_size' do encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: 24) message = encryptor.encrypt(foo: 'bar' * 4) decoded_message = Base64.urlsafe_decode64(message) # slice 1 byte for version, 32 bytes for cipher_secret, 16 bytes for IV # from the start of the string and 32 bytes at the end of the string encrypted_payload = decoded_message[(1 + 32 + 16)..-33] (encrypted_payload.bytesize % 24).must_equal 0 end # This test checks the one-time message key IS NOT used as the cipher key. # Doing so would remove the confidentiality assurances as the key is # essentially included in plaintext then. it 'uses a secret cipher key for encryption and decryption' do cipher = OpenSSL::Cipher.new('aes-256-ctr') encryptor = Rack::Session::Encryptor.new(@secret) message = encryptor.encrypt(foo: 'bar') raw_message = Base64.urlsafe_decode64(message) ver = raw_message.slice!(0, 1) key = raw_message.slice!(0, 32) iv = raw_message.slice!(0, 16) cipher.decrypt cipher.key = key cipher.iv = iv data = cipher.update(raw_message) << cipher.final # "data" should now be random bytes because we tried to decrypt a message # with the wrong key padding_bytes, = data.unpack('v') # likely a large number serialized_data = data.slice(2 + padding_bytes, data.bytesize) # likely nil lambda { Marshal.load serialized_data }.must_raise TypeError end it 'it calls set_cipher_key with the correct key' do encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: 24) message = encryptor.encrypt(foo: 'bar') message_key = Base64.urlsafe_decode64(message).slice(1, 32) callable = proc do |cipher, key| key.wont_equal @secret key.wont_equal message_key cipher.key = key end encryptor.stub :set_cipher_key, callable do encryptor.decrypt message end end end rack-session-2.1.1/test/spec_session_pool.rb000066400000000000000000000253421500636520100211520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. require_relative 'helper' require 'rack/response' require 'rack/mock' require 'rack/utils' require 'rack/lint' require_relative '../lib/rack/session/pool' describe Rack::Session::Pool do session_key = Rack::Session::Pool::DEFAULT_OPTIONS[:key] session_match = /#{session_key}=([0-9a-fA-F]+);/ incrementor = lambda do |env| env["rack.session"]["counter"] ||= 0 env["rack.session"]["counter"] += 1 Rack::Response.new(env["rack.session"].inspect).to_a end get_session_id = Rack::Lint.new(lambda do |env| Rack::Response.new(env["rack.session"].inspect).to_a end) nothing = Rack::Lint.new(lambda do |env| Rack::Response.new("Nothing").to_a end) drop_session = Rack::Lint.new(lambda do |env| env['rack.session.options'][:drop] = true incrementor.call(env) end) renew_session = Rack::Lint.new(lambda do |env| env['rack.session.options'][:renew] = true incrementor.call(env) end) defer_session = Rack::Lint.new(lambda do |env| env['rack.session.options'][:defer] = true incrementor.call(env) end) incrementor = Rack::Lint.new(incrementor) it "creates a new cookie" do pool = Rack::Session::Pool.new(incrementor) res = Rack::MockRequest.new(pool).get("/") res["Set-Cookie"].must_match(session_match) res.body.must_equal ({"counter"=>1}.to_s) end it "determines session from a cookie" do pool = Rack::Session::Pool.new(incrementor) req = Rack::MockRequest.new(pool) cookie = req.get("/")["Set-Cookie"] req.get("/", "HTTP_COOKIE" => cookie). body.must_equal ({"counter"=>2}.to_s) req.get("/", "HTTP_COOKIE" => cookie). body.must_equal ({"counter"=>3}.to_s) end it "survives nonexistent cookies" do pool = Rack::Session::Pool.new(incrementor) res = Rack::MockRequest.new(pool). get("/", "HTTP_COOKIE" => "#{session_key}=blarghfasel") res.body.must_equal ({"counter"=>1}.to_s) end it "does not send the same session id if it did not change" do pool = Rack::Session::Pool.new(incrementor) req = Rack::MockRequest.new(pool) res0 = req.get("/") cookie = res0["Set-Cookie"][session_match] res0.body.must_equal ({"counter"=>1}.to_s) pool.pool.size.must_equal 1 res1 = req.get("/", "HTTP_COOKIE" => cookie) res1["Set-Cookie"].must_be_nil res1.body.must_equal ({"counter"=>2}.to_s) pool.pool.size.must_equal 1 res2 = req.get("/", "HTTP_COOKIE" => cookie) res2["Set-Cookie"].must_be_nil res2.body.must_equal ({"counter"=>3}.to_s) pool.pool.size.must_equal 1 end it "deletes cookies with :drop option" do pool = Rack::Session::Pool.new(incrementor) req = Rack::MockRequest.new(pool) drop = Rack::Utils::Context.new(pool, drop_session) dreq = Rack::MockRequest.new(drop) res1 = req.get("/") session = (cookie = res1["Set-Cookie"])[session_match] res1.body.must_equal ({"counter"=>1}.to_s) pool.pool.size.must_equal 1 res2 = dreq.get("/", "HTTP_COOKIE" => cookie) res2["Set-Cookie"].must_be_nil res2.body.must_equal ({"counter"=>2}.to_s) pool.pool.size.must_equal 0 res3 = req.get("/", "HTTP_COOKIE" => cookie) res3["Set-Cookie"][session_match].wont_equal session res3.body.must_equal ({"counter"=>1}.to_s) pool.pool.size.must_equal 1 end it "provides new session id with :renew option" do pool = Rack::Session::Pool.new(incrementor) req = Rack::MockRequest.new(pool) renew = Rack::Utils::Context.new(pool, renew_session) rreq = Rack::MockRequest.new(renew) res1 = req.get("/") session = (cookie = res1["Set-Cookie"])[session_match] res1.body.must_equal ({"counter"=>1}.to_s) pool.pool.size.must_equal 1 res2 = rreq.get("/", "HTTP_COOKIE" => cookie) new_cookie = res2["Set-Cookie"] new_session = new_cookie[session_match] new_session.wont_equal session res2.body.must_equal ({"counter"=>2}.to_s) pool.pool.size.must_equal 1 res3 = req.get("/", "HTTP_COOKIE" => new_cookie) res3.body.must_equal ({"counter"=>3}.to_s) pool.pool.size.must_equal 1 res4 = req.get("/", "HTTP_COOKIE" => cookie) res4.body.must_equal ({"counter"=>1}.to_s) pool.pool.size.must_equal 2 end it "omits cookie with :defer option" do pool = Rack::Session::Pool.new(incrementor) defer = Rack::Utils::Context.new(pool, defer_session) dreq = Rack::MockRequest.new(defer) res1 = dreq.get("/") res1["Set-Cookie"].must_be_nil res1.body.must_equal ({"counter"=>1}.to_s) pool.pool.size.must_equal 1 end it "can read the session with the legacy id" do pool = Rack::Session::Pool.new(incrementor) req = Rack::MockRequest.new(pool) res0 = req.get("/") cookie = res0["Set-Cookie"] session_id = Rack::Session::SessionId.new cookie[session_match, 1] ses0 = pool.pool[session_id.private_id] pool.pool[session_id.public_id] = ses0 pool.pool.delete(session_id.private_id) res1 = req.get("/", "HTTP_COOKIE" => cookie) res1["Set-Cookie"].must_be_nil res1.body.must_equal(({"counter"=>2}.to_s)) pool.pool[session_id.private_id].wont_be_nil end it "cannot read the session with the legacy id if allow_fallback: false option is used" do pool = Rack::Session::Pool.new(incrementor, allow_fallback: false) req = Rack::MockRequest.new(pool) res0 = req.get("/") cookie = res0["Set-Cookie"] session_id = Rack::Session::SessionId.new cookie[session_match, 1] ses0 = pool.pool[session_id.private_id] pool.pool[session_id.public_id] = ses0 pool.pool.delete(session_id.private_id) res1 = req.get("/", "HTTP_COOKIE" => cookie) res1["Set-Cookie"].wont_be_nil res1.body.must_equal ({"counter"=>1}.to_s) end it "drops the session in the legacy id as well" do pool = Rack::Session::Pool.new(incrementor) req = Rack::MockRequest.new(pool) drop = Rack::Utils::Context.new(pool, drop_session) dreq = Rack::MockRequest.new(drop) res0 = req.get("/") cookie = res0["Set-Cookie"] session_id = Rack::Session::SessionId.new cookie[session_match, 1] ses0 = pool.pool[session_id.private_id] pool.pool[session_id.public_id] = ses0 pool.pool.delete(session_id.private_id) res2 = dreq.get("/", "HTTP_COOKIE" => cookie) res2["Set-Cookie"].must_be_nil res2.body.must_equal ({"counter"=>2}.to_s) pool.pool[session_id.private_id].must_be_nil pool.pool[session_id.public_id].must_be_nil end it "passes through same_site option to session pool" do pool = Rack::Session::Pool.new(incrementor, same_site: :none) pool.same_site.must_equal :none req = Rack::MockRequest.new(pool) res = req.get("/") res["Set-Cookie"].must_match /SameSite=None/i end it "allows using a lambda to specify same_site option, because some browsers require different settings" do pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :none }) req = Rack::MockRequest.new(pool) res = req.get("/") res["Set-Cookie"].must_match /SameSite=None/i pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :lax }) req = Rack::MockRequest.new(pool) res = req.get("/") res["Set-Cookie"].must_match /SameSite=Lax/i end # anyone know how to do this better? it "should merge sessions when multithreaded" do unless $DEBUG 1.must_equal 1 next end warn 'Running multithread tests for Session::Pool' pool = Rack::Session::Pool.new(incrementor) req = Rack::MockRequest.new(pool) res = req.get('/') res.body.must_equal ({"counter"=>1}.to_s) cookie = res["Set-Cookie"] sess_id = cookie[/#{pool.key}=([^,;]+)/, 1] delta_incrementor = lambda do |env| # emulate disconjoinment of threading env['rack.session'] = env['rack.session'].dup Thread.stop env['rack.session'][(Time.now.usec * rand).to_i] = true incrementor.call(env) end tses = Rack::Utils::Context.new pool, delta_incrementor treq = Rack::MockRequest.new(tses) tnum = rand(7).to_i + 5 r = Array.new(tnum) do Thread.new(treq) do |run| run.get('/', "HTTP_COOKIE" => cookie) end end.reverse.map{|t| t.run.join.value } r.each do |resp| resp['Set-Cookie'].must_equal cookie resp.body.must_include '"counter"=>2' end session = pool.pool[sess_id] session.size.must_equal tnum + 1 # counter session['counter'].must_equal 2 # meeeh end it "does not return a cookie if cookie was not read/written" do app = Rack::Session::Pool.new(nothing) res = Rack::MockRequest.new(app).get("/") res["Set-Cookie"].must_be_nil end it "does not return a cookie if cookie was not written (only read)" do app = Rack::Session::Pool.new(get_session_id) res = Rack::MockRequest.new(app).get("/") res["Set-Cookie"].must_be_nil end it "returns even if not read/written if :expire_after is set" do app = Rack::Session::Pool.new(nothing, expire_after: 3600) res = Rack::MockRequest.new(app).get("/", 'rack.session' => { 'not' => 'empty' }) res["Set-Cookie"].wont_be :nil? end it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do app = Rack::Session::Pool.new(nothing, expire_after: 3600) res = Rack::MockRequest.new(app).get("/") res["Set-Cookie"].must_be_nil end user_id_session = Rack::Lint.new(lambda do |env| session = env["rack.session"] case env["PATH_INFO"] when "/login" session[:user_id] = 1 when "/logout" if session[:user_id].nil? raise "User not logged in" end session.delete(:user_id) session.options[:renew] = true when "/slow" Fiber.yield end Rack::Response.new(session.inspect).to_a end) it "doesn't allow session id to be reused" do app = Rack::Session::Pool.new(user_id_session) login_response = Rack::MockRequest.new(app).get("/login") login_cookie = login_response["Set-Cookie"] slow_request = Fiber.new do Rack::MockRequest.new(app).get("/slow", "HTTP_COOKIE" => login_cookie) end slow_request.resume # Check that the session is valid: response = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" => login_cookie) response.body.must_equal({"user_id" => 1}.to_s) logout_response = Rack::MockRequest.new(app).get("/logout", "HTTP_COOKIE" => login_cookie) logout_cookie = logout_response["Set-Cookie"] # Check that the session id is different after logout: login_cookie[session_match].wont_equal logout_cookie[session_match] slow_response = slow_request.resume # Check that the cookie can't be reused: response = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" => login_cookie) response.body.must_equal "{}" end end