@@ -72,7 +72,7 @@ public function setRequesterOption($option, $value)
7272 return $ this ;
7373 }
7474
75- private function signParameters ($ method , $ host , $ path , $ params , $ skey , $ ikey , $ now )
75+ private function signParameters ($ method , $ host , $ path , $ params , $ skey , $ ikey , $ now, $ body , $ additional_headers )
7676 {
7777 assert (is_string ($ method ));
7878 assert (is_string ($ host ));
@@ -82,8 +82,8 @@ private function signParameters($method, $host, $path, $params, $skey, $ikey, $n
8282 assert (is_string ($ ikey ));
8383 assert (is_string ($ now ));
8484
85- $ canon = self ::canonicalize ($ method , $ host , $ path , $ params , $ now );
86-
85+ $ canon = self ::canonicalize ($ method , $ host , $ path , $ params , $ now, $ body , $ additional_headers );
86+
8787 $ signature = self ::sign ($ canon , $ skey );
8888 $ auth = sprintf ("%s:%s " , $ ikey , $ signature );
8989 $ b64auth = base64_encode ($ auth );
@@ -96,25 +96,79 @@ private function sign($msg, $key)
9696 assert (is_string ($ msg ));
9797 assert (is_string ($ key ));
9898
99- return hash_hmac ("sha1 " , $ msg , $ key );
99+ $ msg = mb_convert_encoding ($ msg ?? '' , 'UTF-8 ' , 'ISO-8859-1 ' );
100+ $ key = mb_convert_encoding ($ key ?? '' , 'UTF-8 ' , 'ISO-8859-1 ' );
101+
102+ return hash_hmac ("sha512 " , $ msg , $ key );
100103 }
101104
102- private function canonicalize ($ method , $ host , $ path , $ params , $ now )
105+ private function canonicalize ($ method , $ host , $ path , $ params , $ now, $ body = null , $ additional_headers = [] )
103106 {
104107 assert (is_string ($ method ));
105108 assert (is_string ($ host ));
106109 assert (is_string ($ path ));
107110 assert (is_array ($ params ));
108111 assert (is_string ($ now ));
112+ assert (is_string ($ body ) || $ body === null );
113+ assert (is_array ($ additional_headers ));
109114
110115 $ args = self ::urlEncodeParameters ($ params );
111- $ canon = array ($ now , strtoupper ($ method ), strtolower ($ host ), $ path , $ args );
116+
117+ $ canon = array (
118+ $ now ,
119+ strtoupper ($ method ),
120+ strtolower ($ host ),
121+ $ path ,
122+ $ args ,
123+ hash ('sha512 ' , mb_convert_encoding ($ body ?? '' , 'UTF-8 ' , 'ISO-8859-1 ' )),
124+ self ::canonXDuoHeaders ($ additional_headers ),
125+ );
112126
113127 $ canon = implode ("\n" , $ canon );
114128
115129 return $ canon ;
116130 }
117131
132+ private function canonXDuoHeaders ($ additional_headers = [])
133+ {
134+ assert (is_array ($ additional_headers ));
135+
136+ $ lowered_headers = array_change_key_case ($ additional_headers , CASE_LOWER );
137+ ksort ($ lowered_headers );
138+
139+ $ canon_list = [];
140+ $ added_headers = [];
141+
142+ foreach ($ lowered_headers as $ header_name => $ value ) {
143+ self ::validateAdditionalHeader ($ header_name , $ value , $ added_headers );
144+ array_push ($ canon_list , $ header_name , $ value );
145+ array_push ($ added_headers , $ header_name );
146+ }
147+
148+ $ canon = implode ("\x00" , $ canon_list );
149+ return hash ('sha512 ' , mb_convert_encoding ($ canon ?? '' , 'UTF-8 ' , 'ISO-8859-1 ' ));
150+ }
151+
152+ private function validateAdditionalHeader ($ name , $ value , $ addedHeaders )
153+ {
154+ if ($ name === null || $ value === null )
155+ {
156+ throw new \InvalidArgumentException ("Not allowed 'null' as a header name or value " );
157+ } elseif (str_contains ($ name ,"\x00" ))
158+ {
159+ throw new \InvalidArgumentException ("Not allowed 'Null' character in header name " );
160+ } elseif (str_contains ($ value ,"\x00" ))
161+ {
162+ throw new \InvalidArgumentException ("Not allowed 'Null' character in header value " );
163+ } elseif (!str_starts_with (strtolower ($ name ),"x-duo- " ))
164+ {
165+ throw new \InvalidArgumentException ("Additional headers must start with 'X-Duo-' " );
166+ } elseif (in_array (strtolower ($ name ), $ addedHeaders , true ))
167+ {
168+ throw new \InvalidArgumentException ("Duplicate header passed, header= $ name " );
169+ }
170+ }
171+
118172 private function urlEncodeParameters ($ params )
119173 {
120174 assert (is_array ($ params ));
@@ -149,17 +203,27 @@ private function makeRequest($method, $uri, $body, $headers)
149203 }
150204 }
151205
152- public function apiCall ($ method , $ path , $ params )
206+ public function apiCall ($ method , $ path , $ params, $ additional_headers = [] )
153207 {
154208 assert (is_string ($ method ));
155209 assert (is_string ($ path ));
156210 assert (is_array ($ params ));
211+ assert (is_array ($ additional_headers ));
157212
158213 $ now = date (DateTime::RFC2822 );
159-
160214 $ headers = [];
215+ if (in_array ($ method , ["POST " , "PUT " , "PATCH " ], true )) {
216+ ksort ($ params );
217+ $ body = json_encode ($ params );
218+ $ params = [];
219+ $ headers ["Content-Type " ] = "application/json " ;
220+ $ uri = $ path ;
221+ } else {
222+ $ body = "" ;
223+ $ uri = $ path . (!empty ($ params ) ? "? " . self ::urlEncodeParameters ($ params ) : "" );
224+ }
225+
161226 $ headers ["Date " ] = $ now ;
162- $ headers ["Host " ] = $ this ->host ;
163227 $ headers ["User-Agent " ] = "duo_api_php/ " . VERSION ;
164228 $ headers ["Authorization " ] = self ::signParameters (
165229 $ method ,
@@ -168,19 +232,11 @@ public function apiCall($method, $path, $params)
168232 $ params ,
169233 $ this ->skey ,
170234 $ this ->ikey ,
171- $ now
235+ $ now ,
236+ $ body ,
237+ $ additional_headers ,
172238 );
173239
174- if (in_array ($ method , ["POST " , "PUT " ], true )) {
175- $ body = http_build_query ($ params );
176- $ headers ["Content-Type " ] = "application/x-www-form-urlencoded " ;
177- $ headers ["Content-Length " ] = strval (strlen ($ body ));
178- $ uri = $ path ;
179- } else {
180- $ body = null ;
181- $ uri = $ path . (!empty ($ params ) ? "? " . self ::urlEncodeParameters ($ params ) : "" );
182- }
183-
184240 return self ::makeRequest ($ method , $ uri , $ body , $ headers );
185241 }
186242
0 commit comments