]> git.basschouten.com Git - openhab-addons.git/blob
63df45cd5f6f1f2bd843bc91393700212a9c834d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ipcamera.internal;
14
15 import java.security.MessageDigest;
16 import java.security.NoSuchAlgorithmException;
17 import java.util.Random;
18
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24
25 import io.netty.channel.ChannelDuplexHandler;
26 import io.netty.channel.ChannelHandlerContext;
27 import io.netty.handler.codec.http.HttpResponse;
28
29 /**
30  * The {@link MyNettyAuthHandler} is responsible for handling the basic and digest auths
31  *
32  *
33  * @author Matthew Skinner - Initial contribution
34  */
35
36 @NonNullByDefault
37 public class MyNettyAuthHandler extends ChannelDuplexHandler {
38     public final Logger logger = LoggerFactory.getLogger(getClass());
39     private IpCameraHandler ipCameraHandler;
40     private String username, password;
41     private String httpMethod = "", httpUrl = "";
42     private byte ncCounter = 0;
43     private String nonce = "", opaque = "", qop = "";
44     private String realm = "";
45
46     public MyNettyAuthHandler(String user, String pass, IpCameraHandler handle) {
47         ipCameraHandler = handle;
48         username = user;
49         password = pass;
50     }
51
52     public void setURL(String method, String url) {
53         httpUrl = url;
54         httpMethod = method;
55     }
56
57     private String calcMD5Hash(String toHash) {
58         try {
59             MessageDigest messageDigest = MessageDigest.getInstance("MD5");
60             byte[] array = messageDigest.digest(toHash.getBytes());
61             StringBuffer stringBuffer = new StringBuffer();
62             for (int i = 0; i < array.length; ++i) {
63                 stringBuffer.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3));
64             }
65             return stringBuffer.toString();
66         } catch (NoSuchAlgorithmException e) {
67             logger.warn("NoSuchAlgorithmException error when calculating MD5 hash");
68         }
69         return "";
70     }
71
72     // Method can be used a few ways. processAuth(null, string,string, false) to return the digest on demand, and
73     // processAuth(challString, string,string, true) to auto send new packet
74     // First run it should not have authenticate as null
75     // nonce is reused if authenticate is null so the NC needs to increment to allow this//
76     public void processAuth(String authenticate, String httpMethod, String requestURI, boolean reSend) {
77         if (authenticate.contains("Basic realm=\"")) {
78             if (ipCameraHandler.useDigestAuth) {
79                 // Possible downgrade authenticate attack avoided.
80                 return;
81             }
82             logger.debug("Setting up the camera to use Basic Auth and resending last request with correct auth.");
83             if (ipCameraHandler.setBasicAuth(true)) {
84                 ipCameraHandler.sendHttpRequest(httpMethod, requestURI, null);
85             }
86             return;
87         }
88
89         /////// Fresh Digest Authenticate method follows as Basic is already handled and returned ////////
90         realm = Helper.searchString(authenticate, "realm=\"");
91         if (realm.isEmpty()) {
92             logger.warn("Could not find a valid WWW-Authenticate response in :{}", authenticate);
93             return;
94         }
95         nonce = Helper.searchString(authenticate, "nonce=\"");
96         opaque = Helper.searchString(authenticate, "opaque=\"");
97         qop = Helper.searchString(authenticate, "qop=\"");
98         if (!qop.isEmpty() && !realm.isEmpty()) {
99             ipCameraHandler.useDigestAuth = true;
100         } else {
101             logger.warn(
102                     "!!!! Something is wrong with the reply back from the camera. WWW-Authenticate header: qop:{}, realm:{}",
103                     qop, realm);
104         }
105
106         String stale = Helper.searchString(authenticate, "stale=\"");
107         if ("true".equalsIgnoreCase(stale)) {
108             logger.debug("Camera reported stale=true which normally means the NONCE has expired.");
109         }
110
111         if (password.isEmpty()) {
112             ipCameraHandler.cameraConfigError("Camera gave a 401 reply: You need to provide a password.");
113             return;
114         }
115         // create the MD5 hashes
116         String ha1 = username + ":" + realm + ":" + password;
117         ha1 = calcMD5Hash(ha1);
118         Random random = new Random();
119         String cnonce = Integer.toHexString(random.nextInt());
120         ncCounter = (ncCounter > 125) ? 1 : ++ncCounter;
121         String nc = String.format("%08X", ncCounter); // 8 digit hex number
122         String ha2 = httpMethod + ":" + requestURI;
123         ha2 = calcMD5Hash(ha2);
124
125         String response = ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2;
126         response = calcMD5Hash(response);
127
128         String digestString = "username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\""
129                 + requestURI + "\", cnonce=\"" + cnonce + "\", nc=" + nc + ", qop=\"" + qop + "\", response=\""
130                 + response + "\"";
131         if (!opaque.isEmpty()) {
132             digestString += ", opaque=\"" + opaque + "\"";
133         }
134         if (reSend) {
135             ipCameraHandler.sendHttpRequest(httpMethod, requestURI, digestString);
136             return;
137         }
138     }
139
140     @Override
141     public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
142         if (msg == null || ctx == null) {
143             return;
144         }
145         boolean closeConnection = true;
146         String authenticate = "";
147         if (msg instanceof HttpResponse) {
148             HttpResponse response = (HttpResponse) msg;
149             if (response.status().code() == 401) {
150                 if (!response.headers().isEmpty()) {
151                     for (CharSequence name : response.headers().names()) {
152                         for (CharSequence value : response.headers().getAll(name)) {
153                             if (name.toString().equalsIgnoreCase("WWW-Authenticate")) {
154                                 authenticate = value.toString();
155                             }
156                             if (name.toString().equalsIgnoreCase("Connection")
157                                     && value.toString().contains("keep-alive")) {
158                                 // closeConnection = false;
159                                 // trial this for a while to see if it solves too many bytes with digest turned on.
160                                 closeConnection = true;
161                             }
162                         }
163                     }
164                     if (!authenticate.isEmpty()) {
165                         processAuth(authenticate, httpMethod, httpUrl, true);
166                     } else {
167                         ipCameraHandler.cameraConfigError(
168                                 "Camera gave no WWW-Authenticate: Your login details must be wrong.");
169                     }
170                     if (closeConnection) {
171                         ctx.close();// needs to be here
172                     }
173                 }
174             } else if (response.status().code() != 200) {
175                 logger.debug("Camera at IP:{} gave a reply with a response code of :{}",
176                         ipCameraHandler.cameraConfig.getIp(), response.status().code());
177             }
178         }
179         // Pass the Message back to the pipeline for the next handler to process//
180         super.channelRead(ctx, msg);
181     }
182
183     @Override
184     public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
185     }
186
187     @Override
188     public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
189     }
190 }