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