#include "WebCfgServer.h" #include "PreferencesKeys.h" #include "Version.h" #include "hardware/WifiEthServer.h" WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal) : _server(ethServer), _nuki(nuki), _nukiOpener(nukiOpener), _network(network), _preferences(preferences), _allowRestartToPortal(allowRestartToPortal) { _confirmCode = generateConfirmCode(); String str = _preferences->getString(preference_cred_user); if(str.length() > 0) { _hasCredentials = true; const char *user = str.c_str(); memcpy(&_credUser, user, str.length()); str = _preferences->getString(preference_cred_password); const char *pass = str.c_str(); memcpy(&_credPassword, pass, str.length()); } } void WebCfgServer::initialize() { _server.on("/", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } String response = ""; buildHtml(response); _server.send(200, "text/html", response); }); _server.on("/new.css", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } sendNewCss(); }); _server.on("/inter.css", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } sendFontsInterMinCss(); }); _server.on("/cred", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } String response = ""; buildCredHtml(response); _server.send(200, "text/html", response); }); _server.on("/mqttconfig", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } String response = ""; buildMqttConfigHtml(response); _server.send(200, "text/html", response); }); _server.on("/nukicfg", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } String response = ""; buildNukiConfigHtml(response); _server.send(200, "text/html", response); }); _server.on("/wifi", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } String response = ""; buildConfigureWifiHtml(response); _server.send(200, "text/html", response); }); _server.on("/unpairlock", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } processUnpair(false); }); _server.on("/unpairopener", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } processUnpair(true); }); _server.on("/wifimanager", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } if(_allowRestartToPortal) { String response = ""; buildConfirmHtml(response, "Restarting. Connect to ESP access point to reconfigure WiFi.", 0); _server.send(200, "text/html", response); waitAndProcess(true, 2000); _network->restartAndConfigureWifi(); } }); _server.on("/method=get", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } String message = ""; bool restartEsp = processArgs(message); if(restartEsp) { String response = ""; buildConfirmHtml(response, message); _server.send(200, "text/html", response); Serial.println(F("Restarting")); waitAndProcess(true, 1000); ESP.restart(); } else { String response = ""; buildConfirmHtml(response, message, 3); _server.send(200, "text/html", response); waitAndProcess(false, 1000); } }); _server.on("/ota", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } String response = ""; buildOtaHtml(response); _server.send(200, "text/html", response); }); _server.on("/uploadota", HTTP_POST, [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } _server.send(200, "text/html", ""); }, [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } handleOtaUpload(); }); _server.begin(); } bool WebCfgServer::processArgs(String& message) { bool configChanged = false; bool clearMqttCredentials = false; bool clearCredentials = false; int count = _server.args(); for(int index = 0; index < count; index++) { String key = _server.argName(index); String value = _server.arg(index); if(key == "MQTTSERVER") { _preferences->putString(preference_mqtt_broker, value); configChanged = true; } else if(key == "MQTTPORT") { _preferences->putInt(preference_mqtt_broker_port, value.toInt()); configChanged = true; } else if(key == "MQTTUSER") { if(value == "#") { clearMqttCredentials = true; } else { _preferences->putString(preference_mqtt_user, value); configChanged = true; } } else if(key == "MQTTPASS") { if(value != "*") { _preferences->putString(preference_mqtt_password, value); configChanged = true; } } else if(key == "MQTTPATH") { _preferences->putString(preference_mqtt_lock_path, value); configChanged = true; } else if(key == "MQTTOPPATH") { _preferences->putString(preference_mqtt_opener_path, value); configChanged = true; } else if(key == "MQTTCA") { _preferences->putString(preference_mqtt_ca, value); configChanged = true; } else if(key == "MQTTCRT") { _preferences->putString(preference_mqtt_crt, value); configChanged = true; } else if(key == "MQTTKEY") { _preferences->putString(preference_mqtt_key, value); configChanged = true; } else if(key == "HOSTNAME") { _preferences->putString(preference_hostname, value); configChanged = true; } else if(key == "NETTIMEOUT") { _preferences->putInt(preference_network_timeout, value.toInt()); configChanged = true; } else if(key == "LSTINT") { _preferences->putInt(preference_query_interval_lockstate, value.toInt()); configChanged = true; } else if(key == "BATINT") { _preferences->putInt(preference_query_interval_battery, value.toInt()); configChanged = true; } else if(key == "PRDTMO") { _preferences->putInt(preference_presence_detection_timeout, value.toInt()); configChanged = true; } else if(key == "PUBAUTH") { _preferences->putBool(preference_publish_authdata, (value == "1")); configChanged = true; } else if(key == "LOCKENA") { _preferences->putBool(preference_lock_enabled, (value == "1")); configChanged = true; } else if(key == "OPENA") { _preferences->putBool(preference_opener_enabled, (value == "1")); configChanged = true; } else if(key == "CREDUSER") { if(value == "#") { clearCredentials = true; } else { _preferences->putString(preference_cred_user, value); configChanged = true; } } else if(key == "CREDPASS") { _preferences->putString(preference_cred_password, value); configChanged = true; } else if(key == "NUKIPIN" && _nuki != nullptr) { if(value == "#") { message = "NUKI Lock PIN cleared"; _nuki->setPin(0xffff); } else { message = "NUKI Lock PIN saved"; _nuki->setPin(value.toInt()); } } else if(key == "NUKIOPPIN" && _nukiOpener != nullptr) { if(value == "#") { message = "NUKI Opener PIN cleared"; _nukiOpener->setPin(0xffff); } else { message = "NUKI Opener PIN saved"; _nukiOpener->setPin(value.toInt()); } } } if(clearMqttCredentials) { _preferences->putString(preference_mqtt_user, ""); _preferences->putString(preference_mqtt_password, ""); configChanged = true; } if(clearCredentials) { _preferences->putString(preference_cred_user, ""); _preferences->putString(preference_cred_password, ""); configChanged = true; } if(configChanged) { message = "Configuration saved ... restarting."; _enabled = false; _preferences->end(); } return configChanged; } void WebCfgServer::update() { if(!_enabled) return; _server.handleClient(); } void WebCfgServer::buildHtml(String& response) { buildHtmlHeader(response); response.concat("

Info

\n"); String version = nuki_hub_version; response.concat(""); printParameter(response, "MQTT Connected", _network->isMqttConnected() ? "Yes" : "No"); if(_nuki != nullptr) { char lockstateArr[20]; NukiLock::lockstateToString(_nuki->keyTurnerState().lockState, lockstateArr); printParameter(response, "NUKI Lock paired", _nuki->isPaired() ? "Yes" : "No"); printParameter(response, "NUKI Lock state", lockstateArr); } if(_nukiOpener != nullptr) { char lockstateArr[20]; NukiOpener::lockstateToString(_nukiOpener->keyTurnerState().lockState, lockstateArr); printParameter(response, "NUKI Opener paired", _nukiOpener->isPaired() ? "Yes" : "No"); printParameter(response, "NUKI Opener state", lockstateArr); } printParameter(response, "Firmware", version.c_str()); response.concat("


"); response.concat("

MQTT and Network Configuration

"); buildNavigationButton(response, "Edit", "/mqttconfig"); response.concat("

NUKI Configuration

"); buildNavigationButton(response, "Edit", "/nukicfg"); response.concat("

Credentials

"); buildNavigationButton(response, "Edit", "/cred"); response.concat("

Firmware update

"); buildNavigationButton(response, "Open", "/ota"); if(_allowRestartToPortal) { response.concat("

WiFi

"); buildNavigationButton(response, "Restart and configure wifi", "/wifi"); } response.concat(""); } void WebCfgServer::buildCredHtml(String &response) { buildHtmlHeader(response); response.concat("
"); response.concat("

Credentials

"); response.concat(""); printInputField(response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 20); printInputField(response, "CREDPASS", "Password", "*", 30, true); response.concat("
"); response.concat("
"); response.concat("
"); if(_nuki != nullptr) { response.concat("

"); response.concat("

NUKI Lock PIN

"); response.concat(""); printInputField(response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, true); response.concat("
"); response.concat("
"); response.concat("
"); } if(_nukiOpener != nullptr) { response.concat("

"); response.concat("

NUKI Opener PIN

"); response.concat(""); printInputField(response, "NUKIOPPIN", "PIN Code (# to clear)", "*", 20, true); response.concat("
"); response.concat("
"); response.concat("
"); } _confirmCode = generateConfirmCode(); if(_nuki != nullptr) { response.concat("

Unpair NUKI Lock

"); response.concat("
"); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm unpair"); printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10); response.concat("

"); } if(_nukiOpener != nullptr) { response.concat("

Unpair NUKI Opener

"); response.concat("
"); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm unpair"); printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10); response.concat("

"); } response.concat(""); } void WebCfgServer::buildOtaHtml(String &response) { buildHtmlHeader(response); response.concat("
Choose a file to upload:
"); response.concat("
"); response.concat("
Initiating Over-the-air update. This will take about a minute, please be patient.
You will be forwarwed automatically when the update is complete.
"); response.concat(""); response.concat(""); } void WebCfgServer::buildMqttConfigHtml(String &response) { buildHtmlHeader(response); response.concat("
"); response.concat("

MQTT Configuration

"); response.concat(""); printInputField(response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100); printInputField(response, "MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100); printInputField(response, "MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5); printInputField(response, "MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30); printInputField(response, "MQTTPASS", "MQTT Password", "*", 30, true); printTextarea(response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE); printTextarea(response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE); printTextarea(response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE); printInputField(response, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5); response.concat("
"); response.concat("* If no encryption is configured for the MQTT broker, leave empty.
"); response.concat("
"); response.concat("
"); response.concat(""); } void WebCfgServer::buildNukiConfigHtml(String &response) { buildHtmlHeader(response); response.concat("
"); response.concat("

MQTT Configuration

"); response.concat(""); printCheckBox(response, "LOCKENA", "NUKI Lock enabled", _preferences->getBool(preference_lock_enabled)); if(_preferences->getBool(preference_lock_enabled)) { printInputField(response, "MQTTPATH", "MQTT Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180); } printCheckBox(response, "OPENA", "NUKI Opener enabled", _preferences->getBool(preference_opener_enabled)); if(_preferences->getBool(preference_opener_enabled)) { printInputField(response, "MQTTOPPATH", "MQTT Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180); } printInputField(response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10); printInputField(response, "BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10); printCheckBox(response, "PUBAUTH", "Publish auth data (May reduce battery life)", _preferences->getBool(preference_publish_authdata)); printInputField(response, "PRDTMO", "Presence detection timeout (seconds; -1 to disable)", _preferences->getInt(preference_presence_detection_timeout), 10); response.concat("
"); response.concat("
"); response.concat("
"); response.concat(""); } void WebCfgServer::buildConfirmHtml(String &response, const String &message, uint32_t redirectDelay) { String delay(redirectDelay); response.concat("\n"); response.concat("\n"); response.concat("NUKI Hub\n"); response.concat(""); response.concat("\n\n"); response.concat("\n"); response.concat(message); response.concat(""); } void WebCfgServer::buildConfigureWifiHtml(String &response) { buildHtmlHeader(response); response.concat("

WiFi

"); response.concat("Click confirm to restart ESP into WiFi configuration mode. After restart, connect to ESP access point to reconfigure WiFI.

"); buildNavigationButton(response, "Confirm", "/wifimanager"); response.concat(""); } void WebCfgServer::processUnpair(bool opener) { String response = ""; if(_server.args() == 0) { buildConfirmHtml(response, "Confirm code is invalid.", 3); _server.send(200, "text/html", response); return; } else { String key = _server.argName(0); String value = _server.arg(0); if(key != "CONFIRMTOKEN" || value != _confirmCode) { buildConfirmHtml(response, "Confirm code is invalid.", 3); _server.send(200, "text/html", response); return; } } buildConfirmHtml(response, opener ? "Unpairing NUKI Opener and restarting." : "Unpairing NUKI Lock and restarting.", 3); _server.send(200, "text/html", response); if(!opener && _nuki != nullptr) { _nuki->unpair(); } if(opener && _nukiOpener != nullptr) { _nukiOpener->unpair(); } waitAndProcess(false, 1000); ESP.restart(); } void WebCfgServer::buildHtmlHeader(String &response) { response.concat(""); response.concat(""); response.concat(""); response.concat("NUKI Hub"); } void WebCfgServer::printInputField(String& response, const char *token, const char *description, const char *value, const size_t maxLength, const bool isPassword) { char maxLengthStr[20]; itoa(maxLength, maxLengthStr, 10); response.concat(""); response.concat(description); response.concat(""); response.concat(""); response.concat("\""); } void WebCfgServer::printInputField(String& response, const char *token, const char *description, const int value, size_t maxLength) { char valueStr[20]; itoa(value, valueStr, 10); printInputField(response, token, description, valueStr, maxLength); } void WebCfgServer::printCheckBox(String &response, const char *token, const char *description, const bool value) { response.concat(""); response.concat(description); response.concat(""); response.concat(""); response.concat(""); } void WebCfgServer::printTextarea(String& response, const char *token, const char *description, const char *value, const size_t maxLength) { char maxLengthStr[20]; itoa(maxLength, maxLengthStr, 10); response.concat(""); response.concat(description); response.concat(""); response.concat(" "); response.concat(""); } void WebCfgServer::buildNavigationButton(String &response, const char *caption, const char *targetPath) { response.concat("
"); response.concat(""); response.concat("
"); } void WebCfgServer::printParameter(String& response, const char *description, const char *value) { response.concat(""); response.concat(""); response.concat(description); response.concat(""); response.concat(""); response.concat(value); response.concat(""); response.concat(""); } String WebCfgServer::generateConfirmCode() { int code = random(1000,9999); return String(code); } void WebCfgServer::waitAndProcess(const bool blocking, const uint32_t duration) { unsigned long timeout = millis() + duration; while(millis() < timeout) { _server.handleClient(); if(blocking) { delay(10); } else { vTaskDelay( 50 / portTICK_PERIOD_MS); } } } void WebCfgServer::handleOtaUpload() { if (_server.uri() != "/uploadota") { return; } esp_task_wdt_init(30, false); HTTPUpload& upload = _server.upload(); if(upload.filename == "") { Serial.println("Invalid file for OTA upload"); return; } if (upload.status == UPLOAD_FILE_START) { String filename = upload.filename; if (!filename.startsWith("/")) { filename = "/" + filename; } Serial.print("handleFileUpload Name: "); Serial.println(filename); } else if (upload.status == UPLOAD_FILE_WRITE) { _transferredSize = _transferredSize + upload.currentSize; Serial.println(_transferredSize); _ota.updateFirmware(upload.buf, upload.currentSize); } else if (upload.status == UPLOAD_FILE_END) { Serial.println(); Serial.print("handleFileUpload Size: "); Serial.println(upload.totalSize); } } void WebCfgServer::sendNewCss() { // escaped by https://www.cescaper.com/ _server.send(200, "text/plain", ":root{--nc-font-sans:\\'Inter\\',-apple-system,BlinkMacSystemFont,\\'Segoe UI\\',Roboto,Oxygen,Ubuntu,Cantarell,\\'Open Sans\\',\\'Helvetica Neue\\',sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\",\\\"Segoe UI Symbol\\\";--nc-font-mono:Consolas,monaco,\\'Ubuntu Mono\\',\\'Liberation Mono\\',\\'Courier New\\',Courier,monospace;--nc-tx-1:#000000;--nc-tx-2:#1A1A1A;--nc-bg-1:#FFFFFF;--nc-bg-2:#F6F8FA;--nc-bg-3:#E5E7EB;--nc-lk-1:#0070F3;--nc-lk-2:#0366D6;--nc-lk-tx:#FFFFFF;--nc-ac-1:#79FFE1;--nc-ac-tx:#0C4047}@media (prefers-color-scheme:dark){:root{--nc-tx-1:#ffffff;--nc-tx-2:#eeeeee;--nc-bg-1:#000000;--nc-bg-2:#111111;--nc-bg-3:#222222;--nc-lk-1:#3291FF;--nc-lk-2:#0070F3;--nc-lk-tx:#FFFFFF;--nc-ac-1:#7928CA;--nc-ac-tx:#FFFFFF}}*{margin:0;padding:0}address,area,article,aside,audio,blockquote,datalist,details,dl,fieldset,figure,form,iframe,img,input,meter,nav,ol,optgroup,option,output,p,pre,progress,ruby,section,table,textarea,ul,video{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:break-word;overflow-wrap:break-word;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2)}abbr:hover{cursor:help}blockquote{padding:1.5rem;background:var(--nc-bg-2);border-left:5px solid var(--nc-bg-3)}abbr{cursor:help}blockquote :last-child{padding-bottom:0;margin-bottom:0}header{background:var(--nc-bg-2);border-bottom:1px solid var(--nc-bg-3);padding:2rem 1.5rem;margin:-2rem calc(0px - (50vw - 50%)) 2rem;padding-left:calc(50vw - 50%);padding-right:calc(50vw - 50%)}header h1,header h2,header h3{padding-bottom:0;border-bottom:0}header>:first-child{margin-top:0;padding-top:0}header>:last-child{margin-bottom:0}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}code,kbd,pre,samp{font-family:var(--nc-font-mono)}code,kbd,pre,samp{background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px;padding:3px 6px;font-size:.9rem}kbd{border-bottom:3px solid var(--nc-bg-3)}pre{padding:1rem 1.4rem;max-width:100%;overflow:auto}pre code{background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}code pre{display:inline;background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}details{padding:.6rem 1rem;background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px}summary{cursor:pointer;font-weight:700}details[open]{padding-bottom:.75rem}details[open] summary{margin-bottom:6px}details[open]>:last-child{margin-bottom:0}dt{font-weight:700}dd::before{content:\\'→ \\'}hr{border:0;border-bottom:1px solid var(--nc-bg-3);margin:1rem auto}fieldset{margin-top:1rem;padding:2rem;border:1px solid var(--nc-bg-3);border-radius:4px}legend{padding:auto .5rem}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr:nth-child(even){background:var(--nc-bg-2)}table caption{font-weight:700;margin-bottom:.5rem}textarea{max-width:100%}ol,ul{padding-left:2rem}li{margin-top:.4rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}mark{padding:3px 6px;background:var(--nc-ac-1);color:var(--nc-ac-tx)}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}"); } void WebCfgServer::sendFontsInterMinCss() { // escaped by https://www.cescaper.com/ _server.send(200, "text/plain", "@font-face{font-family:Inter;src:url(src/inter/Inter-Thin.woff2) format(\\'woff2\\'),url(src/inter/Inter-Thin.woff) format(\\'woff\\'),url(src/inter/Inter-Thin.ttf) format(\\'truetype\\');font-weight:100;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-ExtraLight.woff2) format(\\'woff2\\'),url(src/inter/Inter-ExtraLight.woff) format(\\'woff\\'),url(src/inter/Inter-ExtraLight.ttf) format(\\'truetype\\');font-weight:200;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-Light.woff2) format(\\'woff2\\'),url(src/inter/Inter-Light.woff) format(\\'woff\\'),url(src/inter/Inter-Light.ttf) format(\\'truetype\\');font-weight:300;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-Regular.woff2) format(\\'woff2\\'),url(src/inter/Inter-Regular.woff) format(\\'woff\\'),url(src/inter/Inter-Regular.ttf) format(\\'truetype\\');font-weight:400;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-Medium.woff2) format(\\'woff2\\'),url(src/inter/Inter-Medium.woff) format(\\'woff\\'),url(src/inter/Inter-Medium.ttf) format(\\'truetype\\');font-weight:500;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-SemiBold.woff2) format(\\'woff2\\'),url(src/inter/Inter-SemiBold.woff) format(\\'woff\\'),url(src/inter/Inter-SemiBold.ttf) format(\\'truetype\\');font-weight:600;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-Bold.woff2) format(\\'woff2\\'),url(src/inter/Inter-Bold.woff) format(\\'woff\\'),url(src/inter/Inter-Bold.ttf) format(\\'truetype\\');font-weight:700;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-ExtraBold.woff2) format(\\'woff2\\'),url(src/inter/Inter-ExtraBold.woff) format(\\'woff\\'),url(src/inter/Inter-ExtraBold.ttf) format(\\'truetype\\');font-weight:800;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-Black.woff2) format(\\'woff2\\'),url(src/inter/Inter-Black.woff) format(\\'woff\\'),url(src/inter/Inter-Black.ttf) format(\\'truetype\\');font-weight:900;font-style:normal}@font-face{font-family:Inter;src:url(src/inter/Inter-ThinItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-ThinItalic.woff) format(\\'woff\\'),url(src/inter/Inter-ThinItalic.ttf) format(\\'truetype\\');font-weight:100;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-ExtraLightItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-ExtraLightItalic.woff) format(\\'woff\\'),url(src/inter/Inter-ExtraLightItalic.ttf) format(\\'truetype\\');font-weight:200;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-LightItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-LightItalic.woff) format(\\'woff\\'),url(src/inter/Inter-LightItalic.ttf) format(\\'truetype\\');font-weight:300;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-Italic.woff2) format(\\'woff2\\'),url(src/inter/Inter-Italic.woff) format(\\'woff\\'),url(src/inter/Inter-Italic.ttf) format(\\'truetype\\');font-weight:400;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-MediumItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-MediumItalic.woff) format(\\'woff\\'),url(src/inter/Inter-MediumItalic.ttf) format(\\'truetype\\');font-weight:500;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-SemiBoldItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-SemiBoldItalic.woff) format(\\'woff\\'),url(src/inter/Inter-SemiBoldItalic.ttf) format(\\'truetype\\');font-weight:600;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-BoldItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-BoldItalic.woff) format(\\'woff\\'),url(src/inter/Inter-BoldItalic.ttf) format(\\'truetype\\');font-weight:700;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-ExtraBoldItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-ExtraBoldItalic.woff) format(\\'woff\\'),url(src/inter/Inter-ExtraBoldItalic.ttf) format(\\'truetype\\');font-weight:800;font-style:italic}@font-face{font-family:Inter;src:url(src/inter/Inter-BlackItalic.woff2) format(\\'woff2\\'),url(src/inter/Inter-BlackItalic.woff) format(\\'woff\\'),url(src/inter/Inter-BlackItalic.ttf) format(\\'truetype\\');font-weight:900;font-style:italic}"); }