1414
1515import static org .openhab .binding .shelly .internal .ShellyBindingConstants .SHELLY_API_TIMEOUT_MS ;
1616import static org .openhab .binding .shelly .internal .api1 .Shelly1ApiJsonDTO .*;
17+ import static org .openhab .binding .shelly .internal .api2 .Shelly2ApiJsonDTO .*;
1718import static org .openhab .binding .shelly .internal .util .ShellyUtils .*;
1819
1920import java .nio .charset .StandardCharsets ;
21+ import java .text .MessageFormat ;
2022import java .util .Base64 ;
2123import java .util .Map ;
2224import java .util .concurrent .ExecutionException ;
2325import java .util .concurrent .TimeUnit ;
2426import java .util .concurrent .TimeoutException ;
2527
28+ import javax .ws .rs .core .HttpHeaders ;
29+
2630import org .eclipse .jdt .annotation .NonNullByDefault ;
31+ import org .eclipse .jdt .annotation .Nullable ;
2732import org .eclipse .jetty .client .HttpClient ;
2833import org .eclipse .jetty .client .api .ContentResponse ;
2934import org .eclipse .jetty .client .api .Request ;
3237import org .eclipse .jetty .http .HttpHeader ;
3338import org .eclipse .jetty .http .HttpMethod ;
3439import org .eclipse .jetty .http .HttpStatus ;
40+ import org .openhab .binding .shelly .internal .api2 .Shelly2ApiJsonDTO .Shelly2AuthChallenge ;
41+ import org .openhab .binding .shelly .internal .api2 .Shelly2ApiJsonDTO .Shelly2AuthRsp ;
3542import org .openhab .binding .shelly .internal .api2 .Shelly2ApiJsonDTO .Shelly2RpcBaseMessage ;
3643import org .openhab .binding .shelly .internal .config .ShellyThingConfiguration ;
3744import org .openhab .binding .shelly .internal .handler .ShellyThingInterface ;
4956public class ShellyHttpClient {
5057 private final Logger logger = LoggerFactory .getLogger (ShellyHttpClient .class );
5158
52- public static final String HTTP_HEADER_AUTH = "Authorization" ;
59+ public static final String HTTP_HEADER_AUTH = HttpHeaders . AUTHORIZATION ;
5360 public static final String HTTP_AUTH_TYPE_BASIC = "Basic" ;
61+ public static final String HTTP_AUTH_TYPE_DIGEST = "Digest" ;
5462 public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8" ;
5563 public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded" ;
5664
@@ -72,6 +80,7 @@ public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpC
7280 this .thingName = thingName ;
7381 setConfig (thingName , config );
7482 this .httpClient = httpClient ;
83+ this .httpClient .setConnectTimeout (SHELLY_API_TIMEOUT_MS );
7584 }
7685
7786 public void initialize () throws ShellyApiException {
@@ -103,7 +112,7 @@ protected String httpRequest(String uri) throws ShellyApiException {
103112 boolean timeout = false ;
104113 while (retries > 0 ) {
105114 try {
106- apiResult = innerRequest (HttpMethod .GET , uri , "" );
115+ apiResult = innerRequest (HttpMethod .GET , uri , null , "" );
107116 if (timeout ) {
108117 logger .debug ("{}: API timeout #{}/{} recovered ({})" , thingName , timeoutErrors , timeoutsRecovered ,
109118 apiResult .getUrl ());
@@ -128,10 +137,15 @@ protected String httpRequest(String uri) throws ShellyApiException {
128137 }
129138
130139 public String httpPost (String uri , String data ) throws ShellyApiException {
131- return innerRequest (HttpMethod .POST , uri , data ).response ;
140+ return innerRequest (HttpMethod .POST , uri , null , data ).response ;
141+ }
142+
143+ public String httpPost (@ Nullable Shelly2AuthChallenge auth , String data ) throws ShellyApiException {
144+ return innerRequest (HttpMethod .POST , SHELLYRPC_ENDPOINT , auth , data ).response ;
132145 }
133146
134- private ShellyApiResult innerRequest (HttpMethod method , String uri , String data ) throws ShellyApiException {
147+ private ShellyApiResult innerRequest (HttpMethod method , String uri , @ Nullable Shelly2AuthChallenge auth ,
148+ String data ) throws ShellyApiException {
135149 Request request = null ;
136150 String url = "http://" + config .deviceIp + uri ;
137151 ShellyApiResult apiResult = new ShellyApiResult (method .toString (), url );
@@ -140,10 +154,24 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
140154 request = httpClient .newRequest (url ).method (method .toString ()).timeout (SHELLY_API_TIMEOUT_MS ,
141155 TimeUnit .MILLISECONDS );
142156
143- if (!config .password .isEmpty () && !getString (data ).contains ("\" auth\" :{" )) {
144- String value = config .userId + ":" + config .password ;
145- request .header (HTTP_HEADER_AUTH ,
146- HTTP_AUTH_TYPE_BASIC + " " + Base64 .getEncoder ().encodeToString (value .getBytes ()));
157+ if (!uri .equals (SHELLY_URL_DEVINFO ) && !config .password .isEmpty ()) { // not for /shelly or no password
158+ // configured
159+ // Add Auth info
160+ // Gen 1: Basic Auth
161+ // Gen 2: Digest Auth
162+ String authHeader = "" ;
163+ if (auth != null ) { // only if we received an Auth challenge
164+ authHeader = formatAuthResponse (uri ,
165+ buildAuthResponse (uri , auth , SHELLY2_AUTHDEF_USER , config .password ));
166+ } else {
167+ if (!uri .equals (SHELLYRPC_ENDPOINT )) {
168+ String bearer = config .userId + ":" + config .password ;
169+ authHeader = HTTP_AUTH_TYPE_BASIC + " " + Base64 .getEncoder ().encodeToString (bearer .getBytes ());
170+ }
171+ }
172+ if (!authHeader .isEmpty ()) {
173+ request .header (HTTP_HEADER_AUTH , authHeader );
174+ }
147175 }
148176 fillPostData (request , data );
149177 logger .trace ("{}: HTTP {} for {} {}\n {}" , thingName , method , url , data , request .getHeaders ());
@@ -162,14 +190,14 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
162190 apiResult .httpCode = message .error .code ;
163191 apiResult .response = message .error .message ;
164192 if (getInteger (message .error .code ) == HttpStatus .UNAUTHORIZED_401 ) {
165- apiResult .authResponse = getString (message .error .message ).replaceAll ("\\ \" " , "\" " );
193+ apiResult .authChallenge = getString (message .error .message ).replaceAll ("\\ \" " , "\" " );
166194 }
167195 }
168196 }
169197 HttpFields headers = contentResponse .getHeaders ();
170- String auth = headers .get (HttpHeader .WWW_AUTHENTICATE );
171- if (!getString (auth ).isEmpty ()) {
172- apiResult .authResponse = auth ;
198+ String authChallenge = headers .get (HttpHeader .WWW_AUTHENTICATE );
199+ if (!getString (authChallenge ).isEmpty ()) {
200+ apiResult .authChallenge = authChallenge ;
173201 }
174202
175203 // validate response, API errors are reported as Json
@@ -191,6 +219,36 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
191219 return apiResult ;
192220 }
193221
222+ protected @ Nullable Shelly2AuthRsp buildAuthResponse (String uri , @ Nullable Shelly2AuthChallenge challenge ,
223+ String user , String password ) throws ShellyApiException {
224+ if (challenge == null ) {
225+ return null ; // not required
226+ }
227+ if (!SHELLY2_AUTHTTYPE_DIGEST .equalsIgnoreCase (challenge .authType )
228+ || !SHELLY2_AUTHALG_SHA256 .equalsIgnoreCase (challenge .algorithm )) {
229+ throw new IllegalArgumentException ("Unsupported Auth type/algorithm requested by device" );
230+ }
231+ Shelly2AuthRsp response = new Shelly2AuthRsp ();
232+ response .username = user ;
233+ response .realm = challenge .realm ;
234+ response .nonce = challenge .nonce ;
235+ response .cnonce = Long .toHexString ((long ) Math .floor (Math .random () * 10e8 ));
236+ response .nc = "00000001" ;
237+ response .authType = challenge .authType ;
238+ response .algorithm = challenge .algorithm ;
239+ String ha1 = sha256 (response .username + ":" + response .realm + ":" + password );
240+ String ha2 = sha256 (HttpMethod .POST + ":" + uri );// SHELLY2_AUTH_NOISE;
241+ response .response = sha256 (
242+ ha1 + ":" + response .nonce + ":" + response .nc + ":" + response .cnonce + ":" + "auth" + ":" + ha2 );
243+ return response ;
244+ }
245+
246+ protected String formatAuthResponse (String uri , @ Nullable Shelly2AuthRsp rsp ) {
247+ return rsp != null ? MessageFormat .format (HTTP_AUTH_TYPE_DIGEST
248+ + " username=\" {0}\" , realm=\" {1}\" , uri=\" {2}\" , nonce=\" {3}\" , cnonce=\" {4}\" , nc=\" {5}\" , qop=\" auth\" ,response=\" {6}\" , algorithm=\" {7}\" , " ,
249+ rsp .username , rsp .realm , uri , rsp .nonce , rsp .cnonce , rsp .nc , rsp .response , rsp .algorithm ) : "" ;
250+ }
251+
194252 /**
195253 * Fill in POST data, set http headers
196254 *
0 commit comments