Skip to content

Instantly share code, notes, and snippets.

@ancientGlider
Last active February 13, 2026 16:23
Show Gist options
  • Select an option

  • Save ancientGlider/e72cdaa2daf0af5f8d80f53fea4666be to your computer and use it in GitHub Desktop.

Select an option

Save ancientGlider/e72cdaa2daf0af5f8d80f53fea4666be to your computer and use it in GitHub Desktop.
Authentication for Keenetic routers for work with CLI via REST API (Python)
# -*- coding: utf-8 -*-
"""
Данные с адресом, логином, паролем хранятся в конфигурационном файле следующего вида:
[Router]
ip_addr = 192.168.1.1:8080
login = admin
passw = anyPassword
"""
CONFIG_FILE_NAME = "keenetic.conf" # имя конфигурационного файла
import configparser
import requests
import hashlib
cookies_current = None
session = requests.session() # заводим сессию глобально чтобы отрабатывались куки
def keen_auth(login, passw): # авторизация на роутере
response = keen_request(ip_addr, "auth")
if response.status_code == 401:
md5 = login + ":" + response.headers["X-NDM-Realm"] + ":" + passw
md5 = hashlib.md5(md5.encode('utf-8'))
sha = response.headers["X-NDM-Challenge"] + md5.hexdigest()
sha = hashlib.sha256(sha.encode('utf-8'))
response = keen_request(ip_addr, "auth", {"login": login, "password": sha.hexdigest()})
if response.status_code == 200:
return True
elif response.status_code == 200:
return True
else:
return False
def keen_request(ip_addr, query, post = None): # отправка запросов на роутер
global session
# конструируем url
url = "http://" + ip_addr + "/" + query
# если есть данные для запроса POST, делаем POST, иначе GET
if post:
return session.post(url, json=post)
else:
return session.get(url)
config = configparser.ConfigParser() # создаём объекта парсера
config.read(CONFIG_FILE_NAME) # читаем конфиг
ip_addr = config["Router"]["ip_addr"]
login = config["Router"]["login"]
passw = config["Router"]["passw"]
# тестируем
if keen_auth(login, passw):
response = keen_request(ip_addr, 'rci/show/interface/WifiMaster0');
print(response.text)
@Cicaults
Copy link

Cicaults commented Feb 13, 2026

  1. Я так понял, что надо это скрипт оформить в виде, например, serverless функции в яндекс клауде. Ок, сделано. Но надо же, чтобы эта функция ходила на роутер, а значит надо разрешить к роутеру доступ извне - можно по белому IP, но тогда не работает HTTPS, а можно с помощью KeenDNS, но тогда не работает авторизация в приницпе, т.к. не приходят хедеры типа X-NDM-Challenge и X-NDM-Realm. Не понимаю как это победить.

У меня получилась по такой схеме. Посылаем целевой GET\POST запрос, НЕ на /auth, а в нужный нам эндпоинт. В ответе придёт статус 401, со стандартным заголовком для Digest auth схемы, например:
WWW-Authenticate: Digest realm="Keenetic Giga", nonce="y08bAAAAAAAbcGJz80k0UwbEDjHr+/xQDLL5Y2YxOTA=", qop="auth".
Никаких X-NDM-Challenge и X-NDM-Realm.

Дальше вычисляем хеш для ответа, по схеме описанной в примерах выше или с привлечением библиотек. Realm есть, nonce это Challenge
Добавляем в повторный запрос хедер
Authorization: Digest username="userrci", realm="Keenetic Giga", nonce="dFMbAAAAAACt2JFXg3Qs/c1qdue+l3flW4RkVDRlZTQ=", uri="/rci/show/interface/", algorithm="MD5", qop=auth, nc=00000001, cnonce="4nYeHdGL", response="988ea3592ff8584e0d22d246eea32f1c"
где response это то наш хеш.

После этого в успешном ответе от роутера будет хедер Authentication-Info: nextnonce="dFMbAAAAAACt2JFXg3Qs/c1qdue+l3flW4RkVDRlZTQ=", qop=auth, rspauth="36732f6344cac459257be90d1961a660", cnonce="4nYeHdGL", nc=00000001
nextnonce можно использовать для повторного вычисления хеша перед следующим запросом.

Пример кода у меня на джаве с использованием либы Apache httpclient5. Тут без вычисления nextnonce, каждый запрос по новой авторизуется. В итоге получилось сделать serverless функцию в Яндекс.Облаке, связать с телеграм ботом и стрелять на роутер через KeenDNS


    private String keenRequest(ClassicHttpRequest request) {

        try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
            Credentials creds = new UsernamePasswordCredentials(USERNAME, PASSWORD.toCharArray());
            DigestScheme digestScheme = new DigestScheme();
            digestScheme.initPreemptive(creds, null, null);

            // Первый запрос, получаем хедер для авторизации
            String wwwHeader = httpclient.execute(request, response -> {
                if (response.getCode() == 401 && response.getHeader("WWW-Authenticate") != null) {
                    return response.getHeader("WWW-Authenticate").getValue();
                } else {
                    System.err.println("Unexpected return code: " + response.getCode() + "\nheaders: " + Arrays.toString(response.getHeaders()));
                    return "";
                }
            });


            if (wwwHeader.isBlank()) {
                System.err.println("No WWW-Authenticate header found!");
                return null;
            }

            // Парсим хедер, в теории может быть несколько challenges, но у нас всегда один
            List<AuthChallenge> parsedChallenges = AuthChallengeParser.INSTANCE.parse(ChallengeType.TARGET, wwwHeader, new ParserCursor(0, wwwHeader.length()));
            if (parsedChallenges.isEmpty()) {
                System.err.println("Could not parse WWW-Authenticate header!");
                return null;
            }

            // Передаём челледж в digestScheme
            AuthChallenge authChallenge = parsedChallenges.get(0);
            digestScheme.processChallenge(authChallenge, null);

            // Тут происходит магия вычисления хедера Authorization по схеме Digest
            String authResponse = digestScheme.generateAuthResponse(null, request, null);
            request.setHeader("Authorization", authResponse);

            // Повторно отправляем запрос
            return httpclient.execute(request, response -> {
                String responseBody = EntityUtils.toString(response.getEntity());
                if (response.getCode() == 200) {
                    return responseBody;
                }
                return null;
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

Пример вызова

HttpGet httpGet = new HttpGet(String.format("%s/rci/show/interface/", HOST));
String result = this.keenRequest(httpGet);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment