2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal;
15 import java.security.MessageDigest;
16 import java.security.NoSuchAlgorithmException;
17 import java.security.SecureRandom;
18 import java.util.Random;
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;
26 import io.netty.channel.ChannelDuplexHandler;
27 import io.netty.channel.ChannelHandlerContext;
28 import io.netty.handler.codec.http.HttpResponse;
31 * The {@link MyNettyAuthHandler} is responsible for handling the basic and digest auths
34 * @author Matthew Skinner - Initial contribution
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 = "";
47 public MyNettyAuthHandler(String user, String pass, IpCameraHandler handle) {
48 ipCameraHandler = handle;
53 public void setURL(String method, String url) {
58 private String calcMD5Hash(String toHash) {
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));
66 return stringBuffer.toString();
67 } catch (NoSuchAlgorithmException e) {
68 logger.warn("NoSuchAlgorithmException error when calculating MD5 hash");
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.
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);
90 /////// Fresh Digest Authenticate method follows as Basic is already handled and returned ////////
91 realm = Helper.searchString(authenticate, "realm=\"");
92 if (realm.isEmpty()) {
94 "No valid WWW-Authenticate in response. Has the camera activated the illegal login lock? Details:{}",
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;
105 "!!!! Something is wrong with the reply back from the camera. WWW-Authenticate header: qop:{}, realm:{}",
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.");
114 if (password.isEmpty()) {
115 ipCameraHandler.cameraConfigError("Camera gave a 401 reply: You need to provide a password.");
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);
128 String response = ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2;
129 response = calcMD5Hash(response);
131 String digestString = "username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\""
132 + requestURI + "\", cnonce=\"" + cnonce + "\", nc=" + nc + ", qop=\"" + qop + "\", response=\""
134 if (!opaque.isEmpty()) {
135 digestString += ", opaque=\"" + opaque + "\"";
138 ipCameraHandler.sendHttpRequest(httpMethod, requestURI, digestString);
144 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
145 if (msg == null || ctx == null) {
148 if (msg instanceof HttpResponse) {
149 HttpResponse response = (HttpResponse) msg;
150 if (response.status().code() == 401) {
152 if (!response.headers().isEmpty()) {
153 String authenticate = "";
154 for (CharSequence name : response.headers().names()) {
155 for (CharSequence value : response.headers().getAll(name)) {
156 if (name.toString().equalsIgnoreCase("WWW-Authenticate")) {
157 authenticate = value.toString();
161 if (!authenticate.isEmpty()) {
162 processAuth(authenticate, httpMethod, httpUrl, true);
164 ipCameraHandler.cameraConfigError(
165 "Camera gave no WWW-Authenticate: Your login details must be wrong.");
168 } else if (response.status().code() != 200) {
170 switch (response.status().code()) {
173 "403 Forbidden: Check camera setup or has the camera activated the illegal login lock?");
176 logger.debug("Camera at IP:{} gave a reply with a response code of :{}",
177 ipCameraHandler.cameraConfig.getIp(), response.status().code());
182 // Pass the Message back to the pipeline for the next handler to process//
183 super.channelRead(ctx, msg);