11<?php namespace web \unittest \handler ;
22
3- use test \{Assert , Test };
3+ use test \{Assert , Test , Values };
44use util \Bytes ;
55use web \handler \WebSocket ;
66use web \io \{TestInput , TestOutput };
99class WebSocketTest {
1010
1111 /** Handles a request and returns the response generated from the handler */
12- private function handle (Request $ request ): Response {
12+ private function handle (Request $ request, $ origins = [ ' * ' ] ): Response {
1313 $ response = new Response (new TestOutput ());
1414 $ echo = function ($ conn , $ payload ) {
1515 if ($ payload instanceof Bytes) {
@@ -18,10 +18,47 @@ private function handle(Request $request): Response {
1818 $ conn ->send ('Re: ' .$ payload );
1919 }
2020 };
21- (new WebSocket ($ echo ))->handle ($ request , $ response )->next ();
21+ (new WebSocket ($ echo, $ origins ))->handle ($ request , $ response )->next ();
2222 return $ response ;
2323 }
2424
25+ /** @return iterable */
26+ private function same () {
27+
28+ // By default, enforces same-origin policy
29+ yield ['http://localhost:80 ' , 101 ];
30+ yield ['http://localhost ' , 101 ];
31+ yield ['http://Localhost ' , 101 ];
32+
33+ // Not allowed: Differing hosts, ports or scheme
34+ yield ['http://localhost:81 ' , 403 ];
35+ yield ['http://example.localhost ' , 403 ];
36+ yield ['http://localhost.example.com ' , 403 ];
37+ yield ['https://localhost ' , 403 ];
38+ yield ['http://localhost:81@evil.example.com ' , 403 ];
39+ yield ['http://evil.example.com/#http://localhost ' , 403 ];
40+ yield ['http://evil.example.com/?page=http://localhost ' , 403 ];
41+ }
42+
43+ /** @return iterable */
44+ private function origins () {
45+
46+ // We allow all ports and schemes on localhost
47+ yield ['http://localhost ' , 101 ];
48+ yield ['https://localhost ' , 101 ];
49+ yield ['http://localhost:8080 ' , 101 ];
50+ yield ['https://localhost:8443 ' , 101 ];
51+ yield ['http://Localhost ' , 101 ];
52+ yield ['http://api.localhost ' , 101 ];
53+
54+ // Not allowed: Other localhost subdomains and unrelated domains
55+ yield ['http://example.localhost ' , 403 ];
56+ yield ['http://localhost.example.com ' , 403 ];
57+ yield ['http://localhost:81@evil.example.com ' , 403 ];
58+ yield ['http://evil.example.com/#http://localhost ' , 403 ];
59+ yield ['http://evil.example.com/?page=http://localhost ' , 403 ];
60+ }
61+
2562 #[Test]
2663 public function can_create () {
2764 new WebSocket (function ($ conn , $ payload ) { });
@@ -30,16 +67,52 @@ public function can_create() {
3067 #[Test]
3168 public function switching_protocols () {
3269 $ response = $ this ->handle (new Request (new TestInput ('GET ' , '/ws ' , [
70+ 'Origin ' => 'http://localhost:8080 ' ,
3371 'Sec-WebSocket-Version ' => 13 ,
3472 'Sec-WebSocket-Key ' => 'test ' ,
3573 ])));
3674 Assert::equals (101 , $ response ->status ());
3775 Assert::equals ('tNpbgC8ZQDOcSkHAWopKzQjJ1hI= ' , $ response ->headers ()['Sec-WebSocket-Accept ' ]);
3876 }
3977
78+ #[Test]
79+ public function missing_origin () {
80+ $ request = new Request (new TestInput ('GET ' , '/ws ' , [
81+ 'Sec-WebSocket-Version ' => 13 ,
82+ 'Sec-WebSocket-Key ' => 'test ' ,
83+ ]));
84+
85+ Assert::equals (403 , $ this ->handle ($ request )->status ());
86+ }
87+
88+ #[Test, Values(from: 'same ' )]
89+ public function verify_same_origin ($ origin , $ expected ) {
90+ $ request = new Request (new TestInput ('GET ' , '/ws ' , [
91+ 'Origin ' => $ origin ,
92+ 'Host ' => 'localhost:80 ' ,
93+ 'Sec-WebSocket-Version ' => 13 ,
94+ 'Sec-WebSocket-Key ' => 'test ' ,
95+ ]));
96+
97+ Assert::equals ($ expected , $ this ->handle ($ request , [])->status ());
98+ }
99+
100+ #[Test, Values(from: 'origins ' )]
101+ public function verify_localhost_origin ($ origin , $ expected ) {
102+ $ request = new Request (new TestInput ('GET ' , '/ws ' , [
103+ 'Origin ' => $ origin ,
104+ 'Host ' => 'localhost:8080 ' ,
105+ 'Sec-WebSocket-Version ' => 13 ,
106+ 'Sec-WebSocket-Key ' => 'test ' ,
107+ ]));
108+
109+ Assert::equals ($ expected , $ this ->handle ($ request , ['*://localhost:* ' , '*://api.localhost:* ' ])->status ());
110+ }
111+
40112 #[Test]
41113 public function translate_text_message () {
42114 $ response = $ this ->handle (new Request (new TestInput ('POST ' , '/ws ' , [
115+ 'Origin ' => 'http://localhost:8080 ' ,
43116 'Sec-WebSocket-Version ' => 9 ,
44117 'Sec-WebSocket-Id ' => 123 ,
45118 'Content-Type ' => 'text/plain ' ,
@@ -53,6 +126,7 @@ public function translate_text_message() {
53126 #[Test]
54127 public function translate_binary_message () {
55128 $ response = $ this ->handle (new Request (new TestInput ('POST ' , '/ws ' , [
129+ 'Origin ' => 'http://localhost:8080 ' ,
56130 'Sec-WebSocket-Version ' => 9 ,
57131 'Sec-WebSocket-Id ' => 123 ,
58132 'Content-Type ' => 'application/octet-stream ' ,
0 commit comments