Eu não sou programador. Tudo o que garanto é que nas condições certas o programa funciona.
Correções e aperfeiçoamentos são bem-vindos.
Você precisa de:
- Um ESP8266 qualquer – Testado com o ESP-01 de 512KB
- IDE Arduino – Testado com a versão 1.8.1
- “Board” ESP82666 Core – Testado com a versão 2.3.0
- Biblioteca ArduinoJSON
Preencha os dados da sua rede sem fio e da sua conta Cloudflare e faça o upload. Se tudo estiver certo o procedimento do programa será o seguinte:
Ao inicializar:
- Verifica o IP registrado para o host na sua conta Cloudflare;
- Verifica com checkip.dyndns.org qual o seu IP externo atual;
- Se forem diferentes atualiza a sua conta Cloudflare;
- Guarda o IP para não precisar mais consultar a conta;
A cada 10 minutos
- Verifica com checkip.dyndns.org qual o seu IP externo atual;
- Compara com o ip armazenado e atualiza sua conta se forem diferentes;
Em caso de erro o programa reduz o intervalo entre checagens para 2 minutos. Retorna a 10 minutos após o primeiro sucesso.
A melhorar:
- As rotinas de parse ainda são uma gambiarra;
- Não testei se minhas checagens de erro realmente funcionam;
- Com mais alguns GETs e parses JSON é possível obter o zone_id e o host_id a partir do host_name. Isso simplifica a vida de quem não sabe obter essas informações.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
/* * Cloudflare DDNS Updater for ESP8266 * Jefferson Ryan - automalabs.com.br * Testado com IDE v1.8.1 e ESP8266 Core v2.3.0 */ #include <ESP8266WiFi.h> #include <WiFiClientSecure.h> #include <ArduinoJson.h> #include <Ticker.h> Ticker tick; // Você precisa preencher estes valores ======================================================== //sua rede sem fio const char* ssid = ""; const char* password = ""; //cloudflare const String zone_id = ""; const String host_id = ""; const String host_name = ""; const String login_email = ""; const String api_key = ""; //================================================================================================ const char* cloudflare_host = "api.cloudflare.com"; //const String user_agent="Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0"; const String user_agent="ESP8266_DDNS_Updater"; //A Cloudflare exige um user_agent. É educado não mentir se não for indispensável. const int httpsPort = 443; char ipservice[] = "checkip.dyndns.com"; //Obtido através de debug. É diferente do exibido pelo browser //Precisará ser atualizado manualmente quando o certificado expirar const char* cloudflare_host_fingerprint = "6F 1F FB 38 B9 49 03 1E 84 AF DC 48 C8 81 84 45 1A 19 6F 70"; WiFiClientSecure secureClient; WiFiClient UnsecureClient; DynamicJsonBuffer jsonBuffer; String ipExterno; String ipRegistrado; String ipAnterior; boolean refresh=false; const int maxIntervaloChecagens=10; const int minIntervaloChecagens=2; int minutos=0; int intervaloChecagens=maxIntervaloChecagens; void oneMinute() { //oneMinute é uma callback. Executar algo complexo ("blocking") pode levar a resets do ESP8266 minutos++; if (intervaloChecagens-minutos<1){ minutos=0; refresh=true; } else{ int MinutosRestantes=intervaloChecagens-minutos; Serial.print(MinutosRestantes); Serial.println(" minutos para proxima checagem"); } } boolean GETMyIP(){ boolean result=true; Serial.print("Conectando a "); Serial.println(ipservice); if (!UnsecureClient.connect(ipservice, 80)) { Serial.println("Falha na conexao."); result=false; return result; } String url = "/"; UnsecureClient.print("GET " + url + " HTTP/1.1\n"); UnsecureClient.print("Host: " + (String)ipservice+"\n"); UnsecureClient.print("Connection: close\n"); UnsecureClient.print("User-Agent: "+user_agent+"\n"); UnsecureClient.print("\r\n\r\n"); Serial.print("Esperando resposta."); int limiteEspera=500; int count=0; //TODO: Seria melhor usar um FOR aqui? while (UnsecureClient.available() < 1) { //espero por uma resposta count++; Serial.print("."); delay(100); if (count>limiteEspera){ Serial.println("Desisti de esperar."); Serial.println("Fechando Conexao."); UnsecureClient.stop(); return false; break; } } Serial.println(""); //Separo dos "." String line; while(UnsecureClient.available()) //processo a resposta { line = UnsecureClient.readStringUntil('\n'); // Serial.println(line); if (line.substring(0,5)=="<html"){ ipExterno= line.substring(line.indexOf(":")+2); ipExterno= ipExterno.substring(0,ipExterno.indexOf("<")); Serial.print("ipExterno: "); Serial.println(ipExterno); } } Serial.println(""); Serial.println("Fechando conexao."); UnsecureClient.stop(); return result; } boolean conectarCloudflare(){ Serial.print("Conectando a "); Serial.println(cloudflare_host); if (!secureClient.connect(cloudflare_host, httpsPort)) { Serial.println("A conexao falhou"); return false; } if (secureClient.verify(cloudflare_host_fingerprint, cloudflare_host)) { Serial.println("O certificado confere."); return true; } else { Serial.println("O certificado nao confere. Cancelando."); secureClient.stop(); return false; } } boolean SEND(String action){ boolean result=false; if (!conectarCloudflare()){return result;}; String url = "/client/v4/zones/"+zone_id+"/dns_records/"+host_id; Serial.print("Acessando URL: "); Serial.println(url); secureClient.print(action +" " + url + " HTTP/1.1\n"); secureClient.print("Host: " + (String)cloudflare_host+":443\n"); //TODO: colocar a porta em uma constante secureClient.print("Connection: close\n"); secureClient.print("User-Agent: "+user_agent+"\n"); secureClient.print("X-Auth-Email: "+login_email+"\n"); secureClient.print("Content-Type: application/json\n"); secureClient.print("X-Auth-Key: "+api_key+"\n"); if (action=="PUT"){ //String original: {"type":"A","name":"r7.automalabs.com.br","content":"127.0.0.1","ttl":120,"proxied":false} String postData = "{\"type\":\"A\",\"name\":\""+host_name+"\",\"content\":\""+ipExterno+"\",\"ttl\":120,\"proxied\":false}"; secureClient.print("Content-Length: "); secureClient.print(postData.length()); secureClient.print("\r\n\r\n"); secureClient.print(postData); } else { secureClient.print("\r\n\r\n"); } Serial.print(action); Serial.print(" enviado. "); Serial.print("Esperando resposta."); int limiteEspera=500; int count=0; //TODO: Seria melhor usar um FOR aqui? while (secureClient.available() < 1) { //espero por uma resposta count++; Serial.print("."); delay(100); if (count>limiteEspera){ Serial.println("Desisti de esperar."); Serial.println("Fechando Conexao."); secureClient.stop(); return false; break; } } Serial.println(""); //Separo dos "." String line; while(secureClient.available()) //processo a resposta { line = secureClient.readStringUntil('\n'); if (line.substring(2,8)=="result"){ Serial.println(line); String ip= ParseJSON(line.substring(10)); //TODO: fazer processar toda a string if (ip!=""){ ipRegistrado=ip; result=true; } else{ result=false; } } // Serial.println(line); //Util apenas para debug; } Serial.println(""); Serial.println("Fechando conexao."); Serial.println(""); secureClient.stop(); return result; } String ParseJSON(String json){ JsonObject& root = jsonBuffer.parseObject(json); if (!root.success()) { Serial.println("parseObject() falhou"); return ""; } //TODO: fazer o parse de "success" também String nome = root["name"]; String ip = root["content"]; Serial.println(nome); Serial.println(ip); return ip; } void setup() { tick.attach(60, oneMinute); //Executa oneMinute a cada 60 segundos Serial.begin(115200); //Serial.setDebugOutput(true); Serial.println(); Serial.print("Conectando ao AP: "); Serial.println(ssid); Serial.print("Com a senha: "); Serial.println(password); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi conectado."); Serial.print("Endereco IP do ESP8266: "); Serial.println(WiFi.localIP()); if (SEND("GET")){ compararEatualizar(ipRegistrado); } } void loop() { if (refresh) { refresh=false; compararEatualizar(ipAnterior); } } void compararEatualizar(String ip) { if (GETMyIP()){ if (ipExterno==ip){ Serial.println("Os IPs sao iguais."); intervaloChecagens=maxIntervaloChecagens; } else { Serial.print("IP: "); Serial.println(ip); Serial.print("IP externo: "); Serial.println(ipExterno); Serial.println("Os IPs sao diferentes. Iniciando arualizacao."); if (SEND("PUT")){ if (ipExterno!=ipRegistrado){ Serial.println("Erro ao atualizar o IP."); Serial.print("IP na cloudflare: "); Serial.println(ipRegistrado); Serial.print("IP externo: "); Serial.println(ipExterno); intervaloChecagens=minIntervaloChecagens; //PUT falhou } else{ ipAnterior=ipExterno; intervaloChecagens=maxIntervaloChecagens; } } } } else { intervaloChecagens=minIntervaloChecagens; //GetMyIP falhou } Serial.print(intervaloChecagens); Serial.println(" minutos para proxima checagem"); } |
O programa não prossegue se o fingerprint do certificado for diferente do esperado. Infelizmente isso significa que quando o certificado da Clouflare for atualizado o programa deixará de funcionar até que um novo fingerprint seja inserido. Você pode remover esse requerimento mas fica sujeito a roubo de suas credenciais num ataque MITM. Uma sugestão seria armazenar o fingerprint em um arquivo no SPIFFS e fazer o programa ao não conseguir mais validar o certificado mandar uma mensagem para você e passar a procurar por uma atualização de fingerprint no seu domínio, em arquivo criptografado por você.
Infelizmente não parece haver meio do ESP8266 verificar a validade de certificados em tempo real. A checagem de fingerprints foi o melhor que a impressionante equipe por trás do desenvolvimento conseguiu fazer.
Ferramentas online úteis ao debugar esse projeto:
- Teste da ArduinoJSON – Você pode testar algumas idéias sem ter que fazer upload para um arduino
- dostring.com – Não sabe qual o tamanho do JSON em bytes? String Length diz.
- JSON Parser Online – Não consegue visualizar a árvore mentalmente? Permite colapsar estruturas;
Corrigi um erro de comparação quer acontecia ao iniciar e fiz algumas tentativas de tornar o código mais robusto.
Como eu tenho dois acessos à internet hoje que posso escolher trocando o gateway eu tenho também dois IPs externos. Me ocorreu agora que seria útil modificar o programa para que possa atualizar um host com cada IP. Parece ser perfeitamente possível modificar o programa para, usando IP fixo em vez de DHCP, alternar entre os dois gateways e fazer as atualizações
Sim, é possível. Já consegui fazer o meu funcionar.
O programa tem um problema em ParseJSON que faz vazar quantidades aparentemente aleatórias entre execuções (a cada 10 minutos). Numa execução a RAM livre podia continuar a mesma da anterior e na seguinte 4KB serem perdidos de uma vez. O resultado é que em menos de 100 minutos o programa pára de funcionar porque não há mais RAM livre (menos de 9KB) suficiente para jsonBuffer.parseObject operar.
Pode ser que eu esteja chamando a função incorretamente mas eu resolvi o problema eliminando o uso da biblioteca ArduinoJSOn completamente e fazendo o parse da string eu mesmo. O programa parece estável agora com 18KB livres.
Em 4/06/2018 a Cloudflare passou a suportar apenas TLS v1.2 e rejeitar conexões TLS v1.1, que era a versão suportada pelo ESP8266 Core v2.3.0.
Eu obtive sucesso usando o IDE Arduino 1.8.5 e ESP8266 Core 2.4.1, com uma ressalva: o sketch passou a acusar o erro “a conexão falhou” ao tentar acessar api.cloudflare.com.
Ativando o modo de debug no IDE percebi que o erro era um “lookup error: -6” que ocorria na função [hostbyname]. Eu resolvi definindo manualmente um servidor de DNS (usei o da Google) para a chamada de Wifi.config().
Notar que enquanto eu estava usando a versão 2.3.0 eu não observei esses erros de lookup.
O fingerprint atual do certificado Cloudflare é:
E4 D2 7F F4 37 D0 93 2E 29 E6 E6 77 35 A9 0F F9 F0 D4 A5 D3