From e7dd54c1336a44c146e98c40585c6a2d469e82cc Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 29 Mar 2018 10:21:34 +0200 Subject: [PATCH 1/9] #970 added ngrep dumps for mercurial wire protocol and more realistic tests for isWriteRequest --- docs/mercurial/clone-empty.md | 74 +++++++ docs/mercurial/push-bookmark.md | 115 +++++++++++ .../push-multiple-branches-to-new.md | 165 ++++++++++++++++ docs/mercurial/push-multiple-branches.md | 181 ++++++++++++++++++ docs/mercurial/push-single-changeset.md | 146 ++++++++++++++ .../sonia/scm/web/HgPermissionFilterTest.java | 95 ++++++++- .../web/WireProtocolRequestMockFactory.java | 101 ++++++++++ 7 files changed, 872 insertions(+), 5 deletions(-) create mode 100644 docs/mercurial/clone-empty.md create mode 100644 docs/mercurial/push-bookmark.md create mode 100644 docs/mercurial/push-multiple-branches-to-new.md create mode 100644 docs/mercurial/push-multiple-branches.md create mode 100644 docs/mercurial/push-single-changeset.md create mode 100644 scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java diff --git a/docs/mercurial/clone-empty.md b/docs/mercurial/clone-empty.md new file mode 100644 index 0000000000..810a297806 --- /dev/null +++ b/docs/mercurial/clone-empty.md @@ -0,0 +1,74 @@ +# Clone empty repository + +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1efk0qxy1dj5v133hev91zwsf4;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). +. +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1rsxj8u1rq9wizawhyyxok2p5;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3D. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=ewyx4m53d8dajjsob6gxobne;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 42. +Server: Jetty(7.6.21.v20160908). + +0000000000000000000000000000000000000000 +; + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1o0hou15jtiywsywutf30qwm8;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). +. +publishing.True diff --git a/docs/mercurial/push-bookmark.md b/docs/mercurial/push-bookmark.md new file mode 100644 index 0000000000..110bd40cd5 --- /dev/null +++ b/docs/mercurial/push-bookmark.md @@ -0,0 +1,115 @@ +# Push bookmark + +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=7rq9vpp9svfm1sicq7h9vetmv;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +T 172.17.0.2:8080 -> 172.17.0.1:36576 [AP] +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1553csz4sf7scyvw8mqnqfirn;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +ef5993bb4abb32a0565c347844c6d939fc4f4b98 +;1 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=11xa5u3nrmx8k1nar3sazg6jzh;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1p1uzcvfe1pvzh2buzo658rxw;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1mhlj3ucfzdp6ifmzoua4zwit;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=markone&namespace=bookmarks&new=ef5993bb4abb32a0565c347844c6d939fc4f4b98&old=. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=s4vtagb303dv1xg809wnp7e8z;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). +. +1 diff --git a/docs/mercurial/push-multiple-branches-to-new.md b/docs/mercurial/push-multiple-branches-to-new.md new file mode 100644 index 0000000000..56c3a4504a --- /dev/null +++ b/docs/mercurial/push-multiple-branches-to-new.md @@ -0,0 +1,165 @@ +# Push multiple branches to new repository + +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1wu06ykfd4bcv1uv731y4hss2m;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1rajglvqx222g5nppcq3jdfk0;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +0000000000000000000000000000000000000000 +;0 + +GET /scm/hg/hgtest?cmd=known HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: nodes=c0ceccb3b2f0f5c977ff32b9337519e5f37942c2+187ddf37e237c370514487a0bb1a226f11a780b3+b5914611f84eae14543684b2721eec88b0edac12+8b63a323606f10c86b30465570c2574eb7a3a989. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=a5vykp1f0ga2186l8v3gu6lid;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 4. +Server: Jetty(7.6.21.v20160908). + +0000 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=s8lpwqm4c2nqs9kwcg2ca6vm;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1d2qj3kynxlhvk31oli4kk7vf;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: heads=686173686564+6768033e216468247bd031a0a2d9876d79818f8f. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 913. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HG10GZx...oh.U......E.1.....2q.<...s.1.YK*e#..b..{....{..%A..... +,\.....Y.XV....Q/J......`Q/.z.{...<.7....r.s.~?.?..5.~`..?..........O.j.0.....Ih.....!@.P... ..a +;!y..cT...]q.8Zg=...<..,.tq.*.........l........';..w^...w...-......Co..Fs.HYg... +9.F#.P......1..;......D.H.9$@.^....r:E..18...H....3..h...-.=.6l......=q .)."Yg..p\...s@.#.H.*....c8&96..2.GjJ.`.J....r...=Q1..@R.3.o{q...|.......yq.k..,cY..:[... ...S.2...VYp..c5..&.SFR.............V.d..o..........,.. A..M....k...0_.LO1..1"4.;...B....5.9.".U.m.e......]\../p..;?C..W9.........n.~o..gW...Q;..$....S..X.CN.5I].H..!.@...U..J...L.lY.../.-...6.:.Q.'...>.e'..<#3........OL}.52ra[..g*Y:Y....w...=..Z\...S.......tz..;..mf...W......&yUN.r.......4...........`..F...nT..U9................_.~..?...BwzUN.r....B. + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=163487i0ayf9s1k2ng9e1azadj;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 102. +Server: Jetty(7.6.21.v20160908). + +1 +adding changesets +adding manifests +adding file changes +added 5 changesets with 3 changes to 3 files + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=a3i712yjss6t1xsxltnssq0tl;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 58. +Server: Jetty(7.6.21.v20160908). + +c0ceccb3b2f0f5c977ff32b9337519e5f37942c2.1 +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=g8cavdze42d83knmuasrlg10;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). +. +1 diff --git a/docs/mercurial/push-multiple-branches.md b/docs/mercurial/push-multiple-branches.md new file mode 100644 index 0000000000..7d38542fde --- /dev/null +++ b/docs/mercurial/push-multiple-branches.md @@ -0,0 +1,181 @@ +# Push multiple branches + +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1mvm1rxg8333iib7754ksusxc;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=58p9y9vcnz5cjs22dtw8mpwk;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +c0ceccb3b2f0f5c977ff32b9337519e5f37942c2 +;0 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=v5wfwj8k4t261dp6808cdouoa;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=3pgqytfhm4za1dco9p41j9yz5;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +GET /scm/hg/hgtest?cmd=branchmap HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). +. + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1tiz6zf7ui54e1j3d4vouxig5m;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 48. +Server: Jetty(7.6.21.v20160908). + +default c0ceccb3b2f0f5c977ff32b9337519e5f37942c2 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1augu4tc71xax1dit20dtxzkez;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: heads=686173686564+95373ca7cd5371cb6c49bb755ee451d9ec585845. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 746. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HG10GZx...]H.Q...z..r.,.Y..Bw.~..c.Z&...hf.:......e.XK.X,... +,2.E1.B+...(.B"."*..z1.*......M...........93..k|..I..<...h..J_.L.9>.h..@.....op..^.....#....;.*..W....T@....!..dY....jT..A0O6.}..S.2..JPU.O6...aa...rY.VOf9.....7Ukj.&..<...z...j......%}..Jc.8c....k.."9.&".I.P.\..$.At......0..1..g.2.)<..$.. E..dn#....#.Y$3...n...5....J.e.......SNHN.q.MD..4..."I..`PF..?GH1..F..uES..Rl$47.....a........D.1...87.k.t..D..O_.3..6'cN.w.M..|@E.).X!.h*....U.B.X.....h..$.`4... +-..O.:./..oWN.....3...x.L......_[..../..k.R$.x.2..kkv.\2R....4...@.2...1Q..T +..(..m....s.Uo.......{.d.....Y....TYO...S.Pl`a5. ."N$.@...b...qJ.l.).n...1..F.Zy.....&>v;.q.....Jy..X.?.;....>U..|.....d.Y.*.q...NR.3...h.T..x..,.]...p{.^S.S...~..`..q.\j{.oCI.............K.....l9n.s...... + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1e4fnqpncil9z1f7a2pya26nt7;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 102. +Server: Jetty(7.6.21.v20160908). + +1 +adding changesets +adding manifests +adding file changes +added 4 changesets with 2 changes to 2 files + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=f9hvrjssniym1qe33q0u8r2m8;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 101. +Server: Jetty(7.6.21.v20160908). + +b5914611f84eae14543684b2721eec88b0edac12.1 +187ddf37e237c370514487a0bb1a226f11a780b3.1 +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=z5lrut6940a650sw6x9bls8a;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). + +1 diff --git a/docs/mercurial/push-single-changeset.md b/docs/mercurial/push-single-changeset.md new file mode 100644 index 0000000000..19b13c1dcd --- /dev/null +++ b/docs/mercurial/push-single-changeset.md @@ -0,0 +1,146 @@ +# Push single changeset + +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=18r2i2jsba46d14ncsmcjdhaem;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Dc0ceccb3b2f0f5c977ff32b9337519e5f37942c2. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1fw0i0c5zpy281gfgha0f26git;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +0000000000000000000000000000000000000000 +;0 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=dfa46uaqgf39w3jhk857oymu;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=2sk1llvrsagg33xgmwyirfpi;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: heads=686173686564+6768033e216468247bd031a0a2d9876d79818f8f. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 261. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HG10GZx.c``8w.....>|=Y..h.q.....N.......%......Z....&&&.&...YZ.&.&[$.........$.%q..&%..d&.).....%*.....Y.....9z...v\..FF...... +..F..\.z%.%\\.)).) +.P[....D..[un..L).nc..q.m*.H.l#C...eZJ..YJ.Q.qR...e.aJ.EjjJ.AZ..A.Q..E.1.T.'D..C....7s.}..4G........3.S.mL.0.....zk + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=hlucs5utn1ifnpehqmjpt593;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 102. +Server: Jetty(7.6.21.v20160908). + +1 +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files + +T 172.17.0.1:33206 -> 172.17.0.2:8080 [AP] +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=15xomlrxl8qja1cj47rjpqda0y;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 58. +Server: Jetty(7.6.21.v20160908). + +c0ceccb3b2f0f5c977ff32b9337519e5f37942c2.1 +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=c0ceccb3b2f0f5c977ff32b9337519e5f37942c2&namespace=phases&new=0&old=1. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=5zrop5v8e661ipk12tvru525;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). + +1 + diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index 01e01cf302..05250c8cf9 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -39,6 +39,9 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import static org.mockito.Mockito.*; +import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES; +import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.*; + import org.mockito.runners.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.RepositoryProvider; @@ -51,15 +54,14 @@ import sonia.scm.repository.RepositoryProvider; @RunWith(MockitoJUnitRunner.class) public class HgPermissionFilterTest { - @Mock - private HttpServletRequest request; - @Mock private ScmConfiguration configuration; @Mock private RepositoryProvider repositoryProvider; - + + private WireProtocolRequestMockFactory wireProtocol = new WireProtocolRequestMockFactory("/scm/hg/repo"); + @InjectMocks private HgPermissionFilter filter; @@ -82,7 +84,90 @@ public class HgPermissionFilterTest { } private boolean isWriteRequest(String method) { + HttpServletRequest request = mock(HttpServletRequest.class); when(request.getMethod()).thenReturn(method); return filter.isWriteRequest(request); } -} \ No newline at end of file + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * fresh clone of a repository. + */ + @Test + public void testIsWriteRequestWithClone() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES)); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push of a single changeset. + */ + @Test + public void testIsWriteRequestWithSingleChangesetPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2"))); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsWriteRequest(wireProtocol.unbundle(261L, "686173686564+6768033e216468247bd031a0a2d9876d79818f8f")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2&namespace=phases&new=0&old=1")); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push to a single changeset. + */ + @Test + public void testIsWriteRequestWithMultipleChangesetsPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98"))); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsReadRequest(wireProtocol.branchmap()); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsWriteRequest(wireProtocol.unbundle(746L, "686173686564+95373ca7cd5371cb6c49bb755ee451d9ec585845")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1")); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push of multiple branches to a new repository. + */ + @Test + public void testIsWriteRequestWithMutlipleBranchesToNewRepositoryPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98"))); + assertIsReadRequest(wireProtocol.known("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2+187ddf37e237c370514487a0bb1a226f11a780b3+b5914611f84eae14543684b2721eec88b0edac12+8b63a323606f10c86b30465570c2574eb7a3a989")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsWriteRequest(wireProtocol.unbundle(913L, "686173686564+6768033e216468247bd031a0a2d9876d79818f8f")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1")); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push of a bookmark. + */ + @Test + public void testIsWriteRequestWithBookmarkPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98"))); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("markone&namespace=bookmarks&new=ef5993bb4abb32a0565c347844c6d939fc4f4b98&old=")); + } + + private void assertIsReadRequest(HttpServletRequest request) { + assertFalse(filter.isWriteRequest(request)); + } + + private void assertIsWriteRequest(HttpServletRequest request) { + assertTrue(filter.isWriteRequest(request)); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java new file mode 100644 index 0000000000..3d2b6fab92 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java @@ -0,0 +1,101 @@ +package sonia.scm.web; + +import javax.servlet.http.HttpServletRequest; + +import java.util.Locale; + +import static org.mockito.Mockito.*; + +public class WireProtocolRequestMockFactory { + + public enum Namespace { + PHASES, BOOKMARKS; + } + + public static final String CMDS_HEADS_KNOWN_NODES = "heads+%3Bknown+nodes%3D"; + + private String repositoryPath; + + public WireProtocolRequestMockFactory(String repositoryPath) { + this.repositoryPath = repositoryPath; + } + + public HttpServletRequest capabilities() { + return base("GET", "?cmd=capabilities"); + } + + public HttpServletRequest listkeys(Namespace namespace) { + HttpServletRequest request = base("GET", "?cmd=capabilities"); + header(request, "vary", "X-HgArg-1"); + header(request, "x-hgarg-1", namespaceValue(namespace)); + return request; + } + + public HttpServletRequest branchmap() { + return base("GET", "?cmd=branchmap"); + } + + public HttpServletRequest batch(String... args) { + HttpServletRequest request = base("GET", "?cmd=batch"); + args(request, "cmds", args); + return request; + } + + public HttpServletRequest unbundle(long contentLength, String... heads) { + HttpServletRequest request = base("POST", "?cmd=unbundle"); + header(request, "Content-Length", String.valueOf(contentLength)); + args(request, "heads", heads); + return request; + } + + public HttpServletRequest pushkey(String... keys) { + HttpServletRequest request = base("POST", "?cmd=pushkey"); + args(request, "key", keys); + return request; + } + + public HttpServletRequest known(String... nodes) { + HttpServletRequest request = base("POST", "?cmd=pushkey"); + args(request, "nodes", nodes); + return request; + } + + private void args(HttpServletRequest request, String prefix, String[] values) { + StringBuilder vary = new StringBuilder(); + for ( int i=0; i0) { + vary.append(","); + } + vary.append("X-HgArg-" + (i+1)); + header(request, "X-HgArg-" + (i+1), prefix + "=" + values[i]); + } + header(request, "Vary", vary.toString()); + } + + private HttpServletRequest base(String method, String queryStringValue) { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getRequestURI()).thenReturn(repositoryPath); + when(request.getMethod()).thenReturn(method); + + queryString(request, queryStringValue); + + header(request, "Accept", "application/mercurial-0.1"); + header(request, "Accept-Encoding", "identity"); + header(request, "User-Agent", "mercurial/proto-1.0 (Mercurial 4.3.1)"); + return request; + } + + private void queryString(HttpServletRequest request, String queryString) { + when(request.getQueryString()).thenReturn(queryString); + } + + private void header(HttpServletRequest request, String header, String value) { + when(request.getHeader(header)).thenReturn(value); + } + + private String namespaceValue(Namespace namespace) { + return "namespace=" + namespace.toString().toLowerCase(Locale.ENGLISH); + } + +} From 3a9bc6828da2fd8ba30787cc1f10234350062e76 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 29 Mar 2018 19:58:52 +0200 Subject: [PATCH 2/9] use code blocks for request and response --- docs/mercurial/clone-empty.md | 2 ++ docs/mercurial/push-bookmark.md | 2 ++ docs/mercurial/push-multiple-branches-to-new.md | 2 ++ docs/mercurial/push-multiple-branches.md | 2 ++ docs/mercurial/push-single-changeset.md | 3 ++- 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/mercurial/clone-empty.md b/docs/mercurial/clone-empty.md index 810a297806..44a81de20c 100644 --- a/docs/mercurial/clone-empty.md +++ b/docs/mercurial/clone-empty.md @@ -1,5 +1,6 @@ # Clone empty repository +```http GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. Accept-Encoding: identity. accept: application/mercurial-0.1. @@ -72,3 +73,4 @@ Content-Length: 15. Server: Jetty(7.6.21.v20160908). . publishing.True +``` diff --git a/docs/mercurial/push-bookmark.md b/docs/mercurial/push-bookmark.md index 110bd40cd5..9ed591f9f4 100644 --- a/docs/mercurial/push-bookmark.md +++ b/docs/mercurial/push-bookmark.md @@ -1,5 +1,6 @@ # Push bookmark +```http GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. Accept-Encoding: identity. accept: application/mercurial-0.1. @@ -113,3 +114,4 @@ Content-Length: 2. Server: Jetty(7.6.21.v20160908). . 1 +``` diff --git a/docs/mercurial/push-multiple-branches-to-new.md b/docs/mercurial/push-multiple-branches-to-new.md index 56c3a4504a..734c479fef 100644 --- a/docs/mercurial/push-multiple-branches-to-new.md +++ b/docs/mercurial/push-multiple-branches-to-new.md @@ -1,5 +1,6 @@ # Push multiple branches to new repository +```http GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. Accept-Encoding: identity. accept: application/mercurial-0.1. @@ -163,3 +164,4 @@ Content-Length: 2. Server: Jetty(7.6.21.v20160908). . 1 +``` diff --git a/docs/mercurial/push-multiple-branches.md b/docs/mercurial/push-multiple-branches.md index 7d38542fde..5827cb0ceb 100644 --- a/docs/mercurial/push-multiple-branches.md +++ b/docs/mercurial/push-multiple-branches.md @@ -1,5 +1,6 @@ # Push multiple branches +```http GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. Accept-Encoding: identity. accept: application/mercurial-0.1. @@ -179,3 +180,4 @@ Content-Length: 2. Server: Jetty(7.6.21.v20160908). 1 +``` diff --git a/docs/mercurial/push-single-changeset.md b/docs/mercurial/push-single-changeset.md index 19b13c1dcd..499b4c21c3 100644 --- a/docs/mercurial/push-single-changeset.md +++ b/docs/mercurial/push-single-changeset.md @@ -1,5 +1,6 @@ # Push single changeset +```http GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. Accept-Encoding: identity. accept: application/mercurial-0.1. @@ -143,4 +144,4 @@ Content-Length: 2. Server: Jetty(7.6.21.v20160908). 1 - +``` From 8aaa67cd6aff0d1f57fbafae316f5c8bf7ec7be3 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 29 Mar 2018 20:26:56 +0200 Subject: [PATCH 3/9] #970 inspect mercurial commands in order to detect write requests The HgPermissionFilter will now inspect the used mercurial command, of all requests which are using a read method like GET, HEAD, OPTIONS or TRACE and tread every one as write request, expect: - no command was specified with the request (this is required for the hgweb ui) - the command in the query string was found in the list of read commands - if query string contains the batch command, then all commands specified in X-HgArg headers must be in the list of read commands This change is required, in order to fix CVE-2018-1000132 for SCM-Manager. --- .../sonia/scm/web/HgPermissionFilter.java | 9 +- .../main/java/sonia/scm/web/WireProtocol.java | 192 ++++++++++++++++++ .../sonia/scm/web/HgPermissionFilterTest.java | 30 ++- .../web/WireProtocolRequestMockFactory.java | 31 ++- .../java/sonia/scm/web/WireProtocolTest.java | 162 +++++++++++++++ 5 files changed, 403 insertions(+), 21 deletions(-) create mode 100644 scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java create mode 100644 scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index 6700ae3b8d..01fb21885e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -51,13 +51,13 @@ import javax.servlet.http.HttpServletRequest; /** * Permission filter for mercurial repositories. - * + * * @author Sebastian Sdorra */ @Singleton public class HgPermissionFilter extends ProviderPermissionFilter { - + private static final Set READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE"); /** @@ -78,6 +78,9 @@ public class HgPermissionFilter extends ProviderPermissionFilter @Override protected boolean isWriteRequest(HttpServletRequest request) { - return !READ_METHODS.contains(request.getMethod()); + if (READ_METHODS.contains(request.getMethod())) { + return WireProtocol.isWriteRequest(request); + } + return true; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java new file mode 100644 index 0000000000..bab3083445 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2018, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.web; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.util.HttpUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * WireProtocol provides methods for handling the mercurial wire protocol. + * + * @see Mercurial Wire Protocol + */ +public final class WireProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(WireProtocol.class); + + private static final Set READ_COMMANDS = ImmutableSet.of( + "batch", "between", "branchmap", "branches", "capabilities", "changegroup", "changegroupsubset", "clonebundles", + "getbundle", "heads", "hello", "listkeys", "lookup", "known", "stream_out", + // could not find lheads in the wireprotocol description but mercurial 4.5.2 uses it for clone + "lheads" + ); + + private static final Set WRITE_COMMANDS = ImmutableSet.of( + "pushkey", "unbundle" + ); + + private WireProtocol() { + } + + /** + * Returns {@code true} if the request is a write request. The method will always return {@code true}, expect for the + * following cases: + * + * - no command was specified with the request (is required for the hgweb ui) + * - the command in the query string was found in the list of read request + * - if query string contains the batch command, then all commands specified in X-HgArg headers must be + * in the list of read request + * + * @param request http request + * + * @return {@code true} for write requests. + */ + public static boolean isWriteRequest(HttpServletRequest request) { + List commands = commandsOf(request); + boolean write = isWriteRequest(commands); + LOG.trace("mercurial request {} is write: {}", commands, write); + return write; + } + + @VisibleForTesting + static boolean isWriteRequest(List commands) { + return !READ_COMMANDS.containsAll(commands); + } + + @VisibleForTesting + static List commandsOf(HttpServletRequest request) { + List listOfCmds = Lists.newArrayList(); + String cmd = getCommandFromQueryString(request); + if (cmd != null) { + listOfCmds.add(cmd); + if (isBatchCommand(cmd)) { + parseHgArgHeaders(request, listOfCmds); + } + } + return Collections.unmodifiableList(listOfCmds); + } + + private static void parseHgArgHeaders(HttpServletRequest request, List listOfCmds) { + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String header = (String) headerNames.nextElement(); + parseHgArgHeader(request, listOfCmds, header); + } + } + + private static void parseHgArgHeader(HttpServletRequest request, List listOfCmds, String header) { + if (isHgArgHeader(header)) { + String value = getHeaderDecoded(request, header); + if (isHgArgCommandHeader(value)) { + parseHgCommandHeader(listOfCmds, value); + } + } + } + + private static void parseHgCommandHeader(List listOfCmds, String value) { + String[] cmds = value.substring(5).split(";"); + for (String cmd : cmds ) { + String normalizedCmd = normalize(cmd); + int index = normalizedCmd.indexOf(' '); + if (index > 0) { + listOfCmds.add(normalizedCmd.substring(0, index)); + } else { + listOfCmds.add(normalizedCmd); + } + } + } + + private static String normalize(String cmd) { + return cmd.trim().toLowerCase(Locale.ENGLISH); + } + + private static boolean isHgArgCommandHeader(String value) { + return value.startsWith("cmds="); + } + + private static String getHeaderDecoded(HttpServletRequest request, String header) { + return HttpUtil.decode(Strings.nullToEmpty(request.getHeader(header))); + } + + private static boolean isHgArgHeader(String header) { + return header.toLowerCase(Locale.ENGLISH).startsWith("x-hgarg-"); + } + + private static boolean isBatchCommand(String cmd) { + return "batch".equalsIgnoreCase(cmd); + } + + private static String getCommandFromQueryString(HttpServletRequest request) { + // we can't use getParameter, because this would inspect the body for form parameters as well + Multimap queryParameterMap = createQueryParameterMap(request); + + Collection cmd = queryParameterMap.get("cmd"); + Preconditions.checkArgument(cmd.size() <= 1, "found more than one cmd query parameter"); + Iterator iterator = cmd.iterator(); + + String command = null; + if (iterator.hasNext()) { + command = iterator.next(); + } + return command; + } + + private static Multimap createQueryParameterMap(HttpServletRequest request) { + Multimap parameterMap = HashMultimap.create(); + + String queryString = request.getQueryString(); + if (!Strings.isNullOrEmpty(queryString)) { + + String[] parameters = queryString.split("&"); + for (String parameter : parameters) { + int index = parameter.indexOf('='); + if (index > 0) { + parameterMap.put(parameter.substring(0, index), parameter.substring(index + 1)); + } else { + parameterMap.put(parameter, "true"); + } + } + + } + + return parameterMap; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index 05250c8cf9..8319134078 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -1,10 +1,10 @@ /** * Copyright (c) 2014, Sebastian Sdorra * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, @@ -13,7 +13,7 @@ * 3. Neither the name of SCM-Manager; nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,9 +24,9 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + * * http://bitbucket.org/sdorra/scm-manager - * + * */ package sonia.scm.web; @@ -48,7 +48,7 @@ import sonia.scm.repository.RepositoryProvider; /** * Unit tests for {@link HgPermissionFilter}. - * + * * @author Sebastian Sdorra */ @RunWith(MockitoJUnitRunner.class) @@ -56,7 +56,7 @@ public class HgPermissionFilterTest { @Mock private ScmConfiguration configuration; - + @Mock private RepositoryProvider repositoryProvider; @@ -64,7 +64,7 @@ public class HgPermissionFilterTest { @InjectMocks private HgPermissionFilter filter; - + /** * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)}. */ @@ -75,7 +75,7 @@ public class HgPermissionFilterTest { assertFalse(isWriteRequest("HEAD")); assertFalse(isWriteRequest("TRACE")); assertFalse(isWriteRequest("OPTIONS")); - + // write methods assertTrue(isWriteRequest("POST")); assertTrue(isWriteRequest("PUT")); @@ -85,6 +85,7 @@ public class HgPermissionFilterTest { private boolean isWriteRequest(String method) { HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getQueryString()).thenReturn("cmd=capabilities"); when(request.getMethod()).thenReturn(method); return filter.isWriteRequest(request); } @@ -163,6 +164,17 @@ public class HgPermissionFilterTest { assertIsWriteRequest(wireProtocol.pushkey("markone&namespace=bookmarks&new=ef5993bb4abb32a0565c347844c6d939fc4f4b98&old=")); } + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a write request hidden in a batch GET + * request. + * + * @see Issue #970 + */ + @Test + public void testIsWriteRequestWithBookmarkPushInABatch() { + assertIsWriteRequest(wireProtocol.batch("pushkey key=markthree,namespace=bookmarks,new=187ddf37e237c370514487a0bb1a226f11a780b3,old=")); + } + private void assertIsReadRequest(HttpServletRequest request) { assertFalse(filter.isWriteRequest(request)); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java index 3d2b6fab92..d1f5124b3a 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java @@ -1,7 +1,11 @@ package sonia.scm.web; +import com.google.common.collect.Lists; + import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.List; import java.util.Locale; import static org.mockito.Mockito.*; @@ -21,55 +25,64 @@ public class WireProtocolRequestMockFactory { } public HttpServletRequest capabilities() { - return base("GET", "?cmd=capabilities"); + return base("GET", "cmd=capabilities"); } public HttpServletRequest listkeys(Namespace namespace) { - HttpServletRequest request = base("GET", "?cmd=capabilities"); + HttpServletRequest request = base("GET", "cmd=capabilities"); header(request, "vary", "X-HgArg-1"); header(request, "x-hgarg-1", namespaceValue(namespace)); return request; } public HttpServletRequest branchmap() { - return base("GET", "?cmd=branchmap"); + return base("GET", "cmd=branchmap"); } public HttpServletRequest batch(String... args) { - HttpServletRequest request = base("GET", "?cmd=batch"); + HttpServletRequest request = base("GET", "cmd=batch"); args(request, "cmds", args); return request; } public HttpServletRequest unbundle(long contentLength, String... heads) { - HttpServletRequest request = base("POST", "?cmd=unbundle"); + HttpServletRequest request = base("POST", "cmd=unbundle"); header(request, "Content-Length", String.valueOf(contentLength)); args(request, "heads", heads); return request; } public HttpServletRequest pushkey(String... keys) { - HttpServletRequest request = base("POST", "?cmd=pushkey"); + HttpServletRequest request = base("POST", "cmd=pushkey"); args(request, "key", keys); return request; } public HttpServletRequest known(String... nodes) { - HttpServletRequest request = base("POST", "?cmd=pushkey"); + HttpServletRequest request = base("GET", "cmd=known"); args(request, "nodes", nodes); return request; } private void args(HttpServletRequest request, String prefix, String[] values) { + List headers = Lists.newArrayList(); + StringBuilder vary = new StringBuilder(); for ( int i=0; i0) { vary.append(","); } - vary.append("X-HgArg-" + (i+1)); - header(request, "X-HgArg-" + (i+1), prefix + "=" + values[i]); + + vary.append(header); + headers.add(header); + + header(request, header, prefix + "=" + values[i]); } header(request, "Vary", vary.toString()); + + when(request.getHeaderNames()).thenReturn(Collections.enumeration(headers)); } private HttpServletRequest base(String method, String queryStringValue) { diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java new file mode 100644 index 0000000000..860b6a6392 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2018, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.web; + +import com.google.common.collect.Lists; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.http.HttpServletRequest; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link WireProtocol}. + */ +@RunWith(MockitoJUnitRunner.class) +public class WireProtocolTest { + + @Mock + private HttpServletRequest request; + + @Test + public void testIsWriteRequestOnPost() { + assertIsWriteRequest("capabilities", "unbundle"); + } + + @Test + public void testIsWriteRequest() { + assertIsWriteRequest("unbundle"); + assertIsWriteRequest("capabilities", "unbundle"); + assertIsWriteRequest("capabilities", "postkeys"); + assertIsReadRequest(); + assertIsReadRequest("capabilities"); + assertIsReadRequest("capabilities", "branches", "branchmap"); + } + + private void assertIsWriteRequest(String... commands) { + List cmdList = Lists.newArrayList(commands); + assertTrue(WireProtocol.isWriteRequest(cmdList)); + } + + private void assertIsReadRequest(String... commands) { + List cmdList = Lists.newArrayList(commands); + assertFalse(WireProtocol.isWriteRequest(cmdList)); + } + + @Test + public void testGetCommandsOf() { + expectQueryCommand("capabilities", "cmd=capabilities"); + expectQueryCommand("unbundle", "cmd=unbundle"); + expectQueryCommand("unbundle", "prefix=stuff&cmd=unbundle"); + expectQueryCommand("unbundle", "cmd=unbundle&suffix=stuff"); + expectQueryCommand("unbundle", "prefix=stuff&cmd=unbundle&suffix=stuff"); + expectQueryCommand("unbundle", "bool=&cmd=unbundle"); + expectQueryCommand("unbundle", "bool&cmd=unbundle"); + expectQueryCommand("unbundle", "prefix=stu==ff&cmd=unbundle"); + } + + @Test + public void testGetCommandsOfWithBatch() { + prepareBatch("cmds=heads ;known nodes,ef5993bb4abb32a0565c347844c6d939fc4f4b98"); + List commands = WireProtocol.commandsOf(request); + assertThat(commands, contains("batch", "heads", "known")); + } + + @Test + public void testGetCommandsOfWithBatchEncoded() { + prepareBatch("cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98"); + List commands = WireProtocol.commandsOf(request); + assertThat(commands, contains("batch", "heads", "known")); + } + + @Test + public void testGetCommandsOfWithBatchAndMutlipleLines() { + prepareBatch( + "cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98", + "cmds=unbundle; postkeys", + "cmds= branchmap p1=r2,p2=r4; listkeys" + ); + List commands = WireProtocol.commandsOf(request); + assertThat(commands, contains("batch", "heads", "known", "unbundle", "postkeys", "branchmap", "listkeys")); + } + + private void prepareBatch(String... args) { + when(request.getQueryString()).thenReturn("cmd=batch"); + List headers = Lists.newArrayList(); + for (int i=0; i commands = WireProtocol.commandsOf(request); + assertEquals(1, commands.size()); + assertTrue(commands.contains(expected)); + } + +} From a34acd8ed469f2977615139874e7fa2f029ff226 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 29 Mar 2018 22:14:28 +0200 Subject: [PATCH 4/9] #970 added option to enable the experimental httppostargs protocol of mercurial --- .../java/sonia/scm/repository/HgConfig.java | 10 +++++++++ .../main/java/sonia/scm/web/HgCGIServlet.java | 8 ++++++- .../src/main/resources/sonia/scm/hg.config.js | 21 +++++++++++++------ .../main/resources/sonia/scm/python/hgweb.py | 17 +++++++++++---- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java index ed336ff203..2eb060cb66 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java @@ -127,6 +127,10 @@ public class HgConfig extends SimpleRepositoryConfig return disableHookSSLValidation; } + public boolean isEnableHttpPostArgs() { + return enableHttpPostArgs; + } + /** * Method description * @@ -197,6 +201,10 @@ public class HgConfig extends SimpleRepositoryConfig this.showRevisionInId = showRevisionInId; } + public void setEnableHttpPostArgs(boolean enableHttpPostArgs) { + this.enableHttpPostArgs = enableHttpPostArgs; + } + /** * Method description * @@ -232,6 +240,8 @@ public class HgConfig extends SimpleRepositoryConfig /** Field description */ private boolean showRevisionInId = false; + private boolean enableHttpPostArgs = false; + /** * disable validation of ssl certificates for mercurial hook * @see Issue 959 diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java index f6023dd0ae..1fb78161e0 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java @@ -87,6 +87,8 @@ public class HgCGIServlet extends HttpServlet /** Field description */ public static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH"; + private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS"; + /** Field description */ public static final String ENV_SESSION_PREFIX = "SCM_"; @@ -278,11 +280,15 @@ public class HgCGIServlet extends HttpServlet environment.put(ENV_PYTHON_HTTPS_VERIFY, "0"); } + // enable experimental httppostargs protocol of mercurial + // Issue 970: https://goo.gl/poascp + environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs())); + //J- HgEnvironment.prepareEnvironment( environment, handler, - hookManager, + hookManager, request ); //J+ diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js index 58a73ea8e9..f7b4f33240 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js @@ -48,6 +48,7 @@ Sonia.hg.ConfigPanel = Ext.extend(Sonia.config.ConfigForm, { showRevisionInIdText: 'Show Revision', // TODO: i18n disableHookSSLValidationText: 'Disable SSL Validation on Hooks', + enableHttpPostArgsText: 'Enable HttpPostArgs Protocol', // helpText hgBinaryHelpText: 'Location of Mercurial binary.', @@ -63,6 +64,8 @@ Sonia.hg.ConfigPanel = Ext.extend(Sonia.config.ConfigForm, { // TODO: i18n disableHookSSLValidationHelpText: 'Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. \n\ This option should only be used, if SCM-Manager uses a self signed certificate.', + // TODO explain it + enableHttpPostArgsHelpText: 'Enables the experimental HttpPostArgs Protocol.', initComponent: function(){ @@ -115,12 +118,18 @@ Sonia.hg.ConfigPanel = Ext.extend(Sonia.config.ConfigForm, { fieldLabel: this.disableHookSSLValidationText, inputValue: 'true', helpText: this.disableHookSSLValidationHelpText + },{ + xtype: 'checkbox', + name: 'enableHttpPostArgs', + fieldLabel: this.enableHttpPostArgsText, + inputValue: 'true', + helpText: this.enableHttpPostArgsHelpText },{ xtype: 'checkbox', name: 'disabled', fieldLabel: this.disabledText, inputValue: 'true', - helpText: this.disabledHelpText + helpText: this.disabledHelpText },{ xtype: 'button', text: this.configWizardText, @@ -260,11 +269,11 @@ Sonia.repository.typeIcons['hg'] = 'resources/images/icons/16x16/mercurial.png'; // override ChangesetViewerGrid to render changeset id's with revisions Ext.override(Sonia.repository.ChangesetViewerGrid, { - + isMercurialRepository: function(){ return this.repository.type === 'hg'; }, - + getChangesetId: function(id, record){ if ( this.isMercurialRepository() ){ var rev = Sonia.util.getProperty(record.get('properties'), 'hg.rev'); @@ -274,7 +283,7 @@ Ext.override(Sonia.repository.ChangesetViewerGrid, { } return id; }, - + getParentIds: function(id, record){ var parents = record.get('parents'); if ( this.isMercurialRepository() ){ @@ -285,7 +294,7 @@ Ext.override(Sonia.repository.ChangesetViewerGrid, { parents[0] = rev + ':' + parents[0]; } if ( parents.length > 1 ){ - rev = Sonia.util.getProperty(properties, 'hg.p2.rev'); + rev = Sonia.util.getProperty(properties, 'hg.p2.rev'); if (rev){ parents[1] = rev + ':' + parents[1]; } @@ -294,5 +303,5 @@ Ext.override(Sonia.repository.ChangesetViewerGrid, { } return parents; } - + }); diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py index 66d5fadc3c..ff2869044d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py @@ -31,12 +31,21 @@ import os -from mercurial import demandimport +from mercurial import demandimport, ui as uimod, hg from mercurial.hgweb import hgweb, wsgicgi -repositoryPath = os.environ['SCM_REPOSITORY_PATH'] - demandimport.enable() -application = hgweb(repositoryPath) +u = uimod.ui.load() + +# pass SCM_HTTP_POST_ARGS to enable experimental httppostargs protocol of mercurial +# SCM_HTTP_POST_ARGS is set by HgCGIServlet +# Issue 970: https://goo.gl/poascp +u.setconfig('experimental', 'httppostargs', os.environ['SCM_HTTP_POST_ARGS']) + +# open repository +# SCM_REPOSITORY_PATH contains the repository path and is set by HgCGIServlet +r = hg.repository(u, os.environ['SCM_REPOSITORY_PATH']) + +application = hgweb(r) wsgicgi.launch(application) From b43e406b765f6e67d828da608c8dad59e1a14f18 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 30 Mar 2018 11:20:22 +0200 Subject: [PATCH 5/9] #970 initial support of mercurials httppostargs protocol --- .../sonia/scm/web/HgPermissionFilter.java | 36 +++++++++--- .../sonia/scm/web/HgServletInputStream.java | 55 +++++++++++++++++++ .../java/sonia/scm/web/HgServletRequest.java | 31 +++++++++++ .../main/java/sonia/scm/web/WireProtocol.java | 48 ++++++++++++++-- .../sonia/scm/web/HgPermissionFilterTest.java | 48 +++++++++++++--- .../scm/web/HgServletInputStreamTest.java | 50 +++++++++++++++++ .../java/sonia/scm/web/WireProtocolTest.java | 32 ++++++++++- 7 files changed, 279 insertions(+), 21 deletions(-) create mode 100644 scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java create mode 100644 scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java create mode 100644 scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index 01fb21885e..dde048a746 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -38,16 +38,17 @@ package sonia.scm.web; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Singleton; - import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryProvider; import sonia.scm.web.filter.ProviderPermissionFilter; -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Set; - +import javax.servlet.FilterChain; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; /** * Permission filter for mercurial repositories. @@ -60,6 +61,8 @@ public class HgPermissionFilter extends ProviderPermissionFilter private static final Set READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE"); + private final HgRepositoryHandler repositoryHandler; + /** * Constructs a new instance. * @@ -67,17 +70,36 @@ public class HgPermissionFilter extends ProviderPermissionFilter * @param repositoryProvider repository provider */ @Inject - public HgPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repositoryProvider) + public HgPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider, HgRepositoryHandler repositoryHandler) { super(configuration, repositoryProvider); + this.repositoryHandler = repositoryHandler; } //~--- get methods ---------------------------------------------------------- + + @Override + protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + HgServletRequest hgRequest = new HgServletRequest(request); + super.doFilter(hgRequest, response, chain); + // TODO closing stream in case of fire? + } + @Override protected boolean isWriteRequest(HttpServletRequest request) { + if (repositoryHandler.getConfig().isEnableHttpPostArgs()) { + return isHttpPostArgsWriteRequest(request); + } + return isDefaultWriteRequest(request); + } + + private boolean isHttpPostArgsWriteRequest(HttpServletRequest request) { + return WireProtocol.isWriteRequest(request); + } + + private boolean isDefaultWriteRequest(HttpServletRequest request) { if (READ_METHODS.contains(request.getMethod())) { return WireProtocol.isWriteRequest(request); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java new file mode 100644 index 0000000000..b0b2f8ef0d --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java @@ -0,0 +1,55 @@ +package sonia.scm.web; + +import com.google.common.base.Preconditions; + +import javax.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * HgServletInputStream is a wrapper around the original {@link ServletInputStream} and provides some extra + * functionality to support the mercurial client. + */ +public class HgServletInputStream extends ServletInputStream { + + private final ServletInputStream original; + private ByteArrayInputStream captured; + + HgServletInputStream(ServletInputStream original) { + this.original = original; + } + + /** + * Reads the given amount of bytes from the stream and captures them, if the {@link #read()} methods is called the + * captured bytes are returned before the rest of the stream. + * + * @param size amount of bytes to read + * + * @return byte array + * + * @throws IOException if the method is called twice + */ + public byte[] readAndCapture(int size) throws IOException { + Preconditions.checkState(captured == null, "readAndCapture can only be called once per request"); + + // TODO should we enforce a limit? to prevent OOM? + byte[] bytes = new byte[size]; + original.read(bytes); + captured = new ByteArrayInputStream(bytes); + + return bytes; + } + + @Override + public int read() throws IOException { + if (captured != null && captured.available() > 0) { + return captured.read(); + } + return original.read(); + } + + @Override + public void close() throws IOException { + original.close(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java new file mode 100644 index 0000000000..80251c140a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java @@ -0,0 +1,31 @@ +package sonia.scm.web; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.IOException; + +/** + * {@link HttpServletRequestWrapper} which adds some functionality in order to support the mercurial client. + */ +public final class HgServletRequest extends HttpServletRequestWrapper { + + private HgServletInputStream hgServletInputStream; + + /** + * Constructs a request object wrapping the given request. + * + * @param request + * @throws IllegalArgumentException if the request is null + */ + public HgServletRequest(HttpServletRequest request) { + super(request); + } + + @Override + public HgServletInputStream getInputStream() throws IOException { + if (hgServletInputStream == null) { + hgServletInputStream = new HgServletInputStream(super.getInputStream()); + } + return hgServletInputStream; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java index bab3083445..8a411ead64 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java @@ -33,14 +33,17 @@ package sonia.scm.web; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.base.Throwables; import com.google.common.collect.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.util.HttpUtil; import javax.servlet.http.HttpServletRequest; +import java.io.IOException; import java.util.*; /** @@ -73,7 +76,10 @@ public final class WireProtocol { * - no command was specified with the request (is required for the hgweb ui) * - the command in the query string was found in the list of read request * - if query string contains the batch command, then all commands specified in X-HgArg headers must be - * in the list of read request + * in the list of read requests + * - in case of enabled HttpPostArgs protocol and query string container the batch command, the header X-HgArgs-Post + * is read and the commands which are specified in the body from 0 to the value of X-HgArgs-Post must be in the list + * of read requests * * @param request http request * @@ -94,16 +100,40 @@ public final class WireProtocol { @VisibleForTesting static List commandsOf(HttpServletRequest request) { List listOfCmds = Lists.newArrayList(); + String cmd = getCommandFromQueryString(request); if (cmd != null) { listOfCmds.add(cmd); if (isBatchCommand(cmd)) { parseHgArgHeaders(request, listOfCmds); + handleHttpPostArgs(request, listOfCmds); } } return Collections.unmodifiableList(listOfCmds); } + private static void handleHttpPostArgs(HttpServletRequest request, List listOfCmds) { + int hgArgsPostSize = request.getIntHeader("X-HgArgs-Post"); + if (hgArgsPostSize > 0) { + + if (request instanceof HgServletRequest) { + HgServletRequest hgRequest = (HgServletRequest) request; + + try { + byte[] bytes = hgRequest.getInputStream().readAndCapture(hgArgsPostSize); + String hgArgs = new String(bytes, Charsets.US_ASCII); + String decoded = decodeValue(hgArgs); + parseHgCommandHeader(listOfCmds, decoded); + } catch (IOException ex) { + throw Throwables.propagate(ex); + } + } else { + throw new IllegalArgumentException("could not process the httppostargs protocol without HgServletRequest"); + } + + } + } + private static void parseHgArgHeaders(HttpServletRequest request, List listOfCmds) { Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { @@ -115,9 +145,13 @@ public final class WireProtocol { private static void parseHgArgHeader(HttpServletRequest request, List listOfCmds, String header) { if (isHgArgHeader(header)) { String value = getHeaderDecoded(request, header); - if (isHgArgCommandHeader(value)) { - parseHgCommandHeader(listOfCmds, value); - } + parseHgArgValue(listOfCmds, value); + } + } + + private static void parseHgArgValue(List listOfCmds, String value) { + if (isHgArgCommandHeader(value)) { + parseHgCommandHeader(listOfCmds, value); } } @@ -143,7 +177,11 @@ public final class WireProtocol { } private static String getHeaderDecoded(HttpServletRequest request, String header) { - return HttpUtil.decode(Strings.nullToEmpty(request.getHeader(header))); + return decodeValue(request.getHeader(header)); + } + + private static String decodeValue(String value) { + return HttpUtil.decode(Strings.nullToEmpty(value)); } private static boolean isHgArgHeader(String header) { diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index 8319134078..f8aefb95d1 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -31,21 +31,27 @@ package sonia.scm.web; -import javax.servlet.http.HttpServletRequest; +import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; - import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import static org.mockito.Mockito.*; -import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES; -import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.*; - import org.mockito.runners.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryProvider; +import javax.servlet.http.HttpServletRequest; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES; +import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.BOOKMARKS; +import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.PHASES; + /** * Unit tests for {@link HgPermissionFilter}. * @@ -60,11 +66,19 @@ public class HgPermissionFilterTest { @Mock private RepositoryProvider repositoryProvider; + @Mock + private HgRepositoryHandler hgRepositoryHandler; + private WireProtocolRequestMockFactory wireProtocol = new WireProtocolRequestMockFactory("/scm/hg/repo"); @InjectMocks private HgPermissionFilter filter; + @Before + public void setUp() { + when(hgRepositoryHandler.getConfig()).thenReturn(new HgConfig()); + } + /** * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)}. */ @@ -83,9 +97,27 @@ public class HgPermissionFilterTest { assertTrue(isWriteRequest("KA")); } + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with enabled httppostargs option. + */ + @Test + public void testIsWriteRequestWithEnabledHttpPostArgs() { + HgConfig config = new HgConfig(); + config.setEnableHttpPostArgs(true); + when(hgRepositoryHandler.getConfig()).thenReturn(config); + + assertFalse(isWriteRequest("POST")); + assertFalse(isWriteRequest("POST", "heads")); + assertTrue(isWriteRequest("POST", "unbundle")); + } + private boolean isWriteRequest(String method) { + return isWriteRequest(method, "capabilities"); + } + + private boolean isWriteRequest(String method, String command) { HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getQueryString()).thenReturn("cmd=capabilities"); + when(request.getQueryString()).thenReturn("cmd=" + command); when(request.getMethod()).thenReturn(method); return filter.isWriteRequest(request); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java new file mode 100644 index 0000000000..51b0a050fc --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java @@ -0,0 +1,50 @@ +package sonia.scm.web; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; +import org.junit.Test; + +import javax.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class HgServletInputStreamTest { + + @Test + public void testReadAndCapture() throws IOException { + SampleServletInputStream original = new SampleServletInputStream("trillian.mcmillian@hitchhiker.com"); + HgServletInputStream hgServletInputStream = new HgServletInputStream(original); + + byte[] prefix = hgServletInputStream.readAndCapture(8); + assertEquals("trillian", new String(prefix, Charsets.US_ASCII)); + + byte[] wholeBytes = ByteStreams.toByteArray(hgServletInputStream); + assertEquals("trillian.mcmillian@hitchhiker.com", new String(wholeBytes, Charsets.US_ASCII)); + } + + @Test(expected = IllegalStateException.class) + public void testReadAndCaptureCalledTwice() throws IOException { + SampleServletInputStream original = new SampleServletInputStream("trillian.mcmillian@hitchhiker.com"); + HgServletInputStream hgServletInputStream = new HgServletInputStream(original); + + hgServletInputStream.readAndCapture(1); + hgServletInputStream.readAndCapture(1); + } + + private static class SampleServletInputStream extends ServletInputStream { + + private ByteArrayInputStream input; + + private SampleServletInputStream(String data) { + input = new ByteArrayInputStream(data.getBytes()); + } + + @Override + public int read() { + return input.read(); + } + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java index 860b6a6392..519dadfd6c 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java @@ -32,14 +32,17 @@ package sonia.scm.web; +import com.google.common.base.Charsets; import com.google.common.collect.Lists; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; - +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -93,6 +96,18 @@ public class WireProtocolTest { expectQueryCommand("unbundle", "prefix=stu==ff&cmd=unbundle"); } + @Test + public void testGetCommandsOfWithHgArgsPost() throws IOException { + when(request.getMethod()).thenReturn("POST"); + when(request.getQueryString()).thenReturn("cmd=batch"); + when(request.getIntHeader("X-HgArgs-Post")).thenReturn(29); + when(request.getHeaderNames()).thenReturn(Collections.enumeration(Lists.newArrayList("X-HgArgs-Post"))); + when(request.getInputStream()).thenReturn(new BufferedServletInputStream("cmds=lheads+%3Bknown+nodes%3D")); + + List commands = WireProtocol.commandsOf(new HgServletRequest(request)); + assertThat(commands, contains("batch", "lheads", "known")); + } + @Test public void testGetCommandsOfWithBatch() { prepareBatch("cmds=heads ;known nodes,ef5993bb4abb32a0565c347844c6d939fc4f4b98"); @@ -159,4 +174,19 @@ public class WireProtocolTest { assertTrue(commands.contains(expected)); } + private static class BufferedServletInputStream extends ServletInputStream { + + private ByteArrayInputStream input; + + BufferedServletInputStream(String content) { + this.input = new ByteArrayInputStream(content.getBytes(Charsets.US_ASCII)); + } + + @Override + public int read() { + return input.read(); + } + + } + } From 8047d360285248122cb8dbb5e13d19e617859b6d Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 3 Apr 2018 11:00:16 +0200 Subject: [PATCH 6/9] #970 use iso-8859-1 for http post args instead of us-ascii --- .../main/java/sonia/scm/web/WireProtocol.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java index 8a411ead64..fb84692805 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java @@ -119,14 +119,7 @@ public final class WireProtocol { if (request instanceof HgServletRequest) { HgServletRequest hgRequest = (HgServletRequest) request; - try { - byte[] bytes = hgRequest.getInputStream().readAndCapture(hgArgsPostSize); - String hgArgs = new String(bytes, Charsets.US_ASCII); - String decoded = decodeValue(hgArgs); - parseHgCommandHeader(listOfCmds, decoded); - } catch (IOException ex) { - throw Throwables.propagate(ex); - } + parseHttpPostArgs(listOfCmds, hgArgsPostSize, hgRequest); } else { throw new IllegalArgumentException("could not process the httppostargs protocol without HgServletRequest"); } @@ -134,6 +127,19 @@ public final class WireProtocol { } } + private static void parseHttpPostArgs(List listOfCmds, int hgArgsPostSize, HgServletRequest hgRequest) { + try { + byte[] bytes = hgRequest.getInputStream().readAndCapture(hgArgsPostSize); + // we use iso-8859-1 for encoding, because the post args are normally http headers which are using iso-8859-1 + // see https://tools.ietf.org/html/rfc7230#section-3.2.4 + String hgArgs = new String(bytes, Charsets.ISO_8859_1); + String decoded = decodeValue(hgArgs); + parseHgCommandHeader(listOfCmds, decoded); + } catch (IOException ex) { + throw Throwables.propagate(ex); + } + } + private static void parseHgArgHeaders(HttpServletRequest request, List listOfCmds) { Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { From acebd0f25e28955141a671a087deba7dd05bbcff Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 3 Apr 2018 11:14:05 +0200 Subject: [PATCH 7/9] #970 wrap requests only if http postargs is enabled --- .../sonia/scm/web/HgPermissionFilter.java | 19 ++++++++++++---- .../sonia/scm/web/HgPermissionFilterTest.java | 22 +++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index dde048a746..955002928c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -35,6 +35,7 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -81,20 +82,30 @@ public class HgPermissionFilter extends ProviderPermissionFilter @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - HgServletRequest hgRequest = new HgServletRequest(request); - super.doFilter(hgRequest, response, chain); - // TODO closing stream in case of fire? + super.doFilter(wrapRequestIfRequired(request), response, chain); + } + + @VisibleForTesting + HttpServletRequest wrapRequestIfRequired(HttpServletRequest request) { + if (isHttpPostArgsEnabled()) { + return new HgServletRequest(request); + } + return request; } @Override protected boolean isWriteRequest(HttpServletRequest request) { - if (repositoryHandler.getConfig().isEnableHttpPostArgs()) { + if (isHttpPostArgsEnabled()) { return isHttpPostArgsWriteRequest(request); } return isDefaultWriteRequest(request); } + private boolean isHttpPostArgsEnabled() { + return repositoryHandler.getConfig().isEnableHttpPostArgs(); + } + private boolean isHttpPostArgsWriteRequest(HttpServletRequest request) { return WireProtocol.isWriteRequest(request); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index f8aefb95d1..bb6692ffce 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -44,8 +44,9 @@ import sonia.scm.repository.RepositoryProvider; import javax.servlet.http.HttpServletRequest; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES; @@ -60,6 +61,9 @@ import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.PHASES; @RunWith(MockitoJUnitRunner.class) public class HgPermissionFilterTest { + @Mock + private HttpServletRequest request; + @Mock private ScmConfiguration configuration; @@ -79,6 +83,20 @@ public class HgPermissionFilterTest { when(hgRepositoryHandler.getConfig()).thenReturn(new HgConfig()); } + /** + * Tests {@link HgPermissionFilter#wrapRequestIfRequired(HttpServletRequest)}. + */ + @Test + public void testWrapRequestIfRequired() { + assertSame(request, filter.wrapRequestIfRequired(request)); + + HgConfig hgConfig = new HgConfig(); + hgConfig.setEnableHttpPostArgs(true); + when(hgRepositoryHandler.getConfig()).thenReturn(hgConfig); + + assertThat(filter.wrapRequestIfRequired(request), is(instanceOf(HgServletRequest.class))); + } + /** * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)}. */ From 3d401b93ea6fbed8d3bef13e6ad7e079ac3d993d Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 3 Apr 2018 11:56:51 +0200 Subject: [PATCH 8/9] #970 added help text for enable httppostargs --- .../scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js index f7b4f33240..b10de4b422 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js @@ -65,7 +65,9 @@ Sonia.hg.ConfigPanel = Ext.extend(Sonia.config.ConfigForm, { disableHookSSLValidationHelpText: 'Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. \n\ This option should only be used, if SCM-Manager uses a self signed certificate.', // TODO explain it - enableHttpPostArgsHelpText: 'Enables the experimental HttpPostArgs Protocol.', + enableHttpPostArgsHelpText: 'Enables the experimental HttpPostArgs Protocol of mercurial.\n\ + The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers.\ + This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.', initComponent: function(){ From 473f3257a0c068ea5807f72a3e3b5306b82caa8b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 5 Apr 2018 18:44:42 +0200 Subject: [PATCH 9/9] close branch issue-970