463 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			463 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include <SmingCore.h>
 | |
| #include <Data/WebHelpers/base64.h>
 | |
| #include <ArduinoJson.h>
 | |
| #include "utils.h"
 | |
| #include "main.h"
 | |
| 
 | |
| CMain g_Main;
 | |
| 
 | |
| CMain::CMain() : m_FingerLogic(this)
 | |
| {
 | |
| }
 | |
| 
 | |
| static void STAGotIP(IpAddress ip, IpAddress mask, IpAddress gateway)
 | |
| {
 | |
| 	debugf("GOTIP - IP: %s, MASK: %s, GW: %s\n", ip.toString().c_str(), mask.toString().c_str(),
 | |
| 			gateway.toString().c_str());
 | |
| }
 | |
| 
 | |
| static void STADisconnect(const String& ssid, MacAddress bssid, WifiDisconnectReason reason)
 | |
| {
 | |
| 	debugf("DISCONNECT - SSID: %s, REASON: %d", ssid.c_str(), WifiEvents.getDisconnectReasonDesc(reason).c_str());
 | |
| }
 | |
| 
 | |
| void CMain::Init(HardwareSerial &serial)
 | |
| {
 | |
| 	m_Settings.Load();
 | |
| 	//m_Settings.Save();
 | |
| 
 | |
| 	WifiAccessPoint.enable(false);
 | |
| 	if(m_Settings.m_aSSID[0])
 | |
| 	{
 | |
| 		debugf("Station: %s", m_Settings.m_aSSID);
 | |
| 		WifiStation.enable(true);
 | |
| 		WifiStation.config(m_Settings.m_aSSID, m_Settings.m_aPSK);
 | |
| 		if(m_Settings.m_DHCP && m_Settings.m_aHostname[0])
 | |
| 			WifiStation.setHostname(m_Settings.m_aHostname);
 | |
| 		if(!m_Settings.m_DHCP && !m_Settings.m_Address.isNull())
 | |
| 			WifiStation.setIP(m_Settings.m_Address, m_Settings.m_Netmask, m_Settings.m_Gateway);
 | |
| 		WifiStation.connect();
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		debugf("Access Point 'admin': %s", m_Settings.m_aPassword);
 | |
| 		WifiAccessPoint.config("safeweb", m_Settings.m_aPassword, AUTH_WPA2_PSK);
 | |
| 		WifiAccessPoint.enable(true);
 | |
| 	}
 | |
| 
 | |
| 	WifiEvents.onStationGotIP(STAGotIP);
 | |
| 	WifiEvents.onStationDisconnect(STADisconnect);
 | |
| 
 | |
| 	m_FTP.listen(21);
 | |
| 	m_FTP.addUser(m_Settings.m_aUsername, m_Settings.m_aPassword);
 | |
| 
 | |
| 	m_HttpServer.listen(80);
 | |
| 	m_HttpServer.setBodyParser("application/json", bodyToStringParser);
 | |
| 
 | |
| 	m_HttpServer.paths.set("/api", HttpPathDelegate(&CMain::HttpOnApi, this));
 | |
| 	m_HttpServer.paths.set("/api/dashboard", HttpPathDelegate(&CMain::HttpOnApi, this));
 | |
| 	m_HttpServer.paths.set("/api/fingerprint", HttpPathDelegate(&CMain::HttpOnApi, this));
 | |
| 	m_HttpServer.paths.set("/api/fingerprint/label", HttpPathDelegate(&CMain::HttpOnApi, this));
 | |
| 	m_HttpServer.paths.set("/api/fingerprint/delete", HttpPathDelegate(&CMain::HttpOnApi, this));
 | |
| 	m_HttpServer.paths.set("/api/fingerprint/enroll", HttpPathDelegate(&CMain::HttpOnApi, this));
 | |
| 	m_HttpServer.paths.set("/api/unlock", HttpPathDelegate(&CMain::HttpOnApi, this));
 | |
| 
 | |
| 	m_HttpServer.paths.setDefault(HttpPathDelegate(&CMain::HttpOnFile, this));
 | |
| 
 | |
| 	m_FingerPrint.Init(serial, 0xFFFFFFFF, 0x00000000);
 | |
| 	m_FingerLogic.Init(&m_FingerPrint);
 | |
| }
 | |
| 
 | |
| bool CMain::HttpAuthorized(HttpRequest &request, HttpResponse &response)
 | |
| {
 | |
| 	String auth = request.getHeader("Authorization");
 | |
| 	if(auth.startsWith("Basic "))
 | |
| 	{
 | |
| 		int headerLength = auth.length() - 6;
 | |
| 		if(headerLength <= 64)
 | |
| 		{
 | |
| 			auth = base64_decode(auth.c_str() + 6, headerLength);
 | |
| 			if(auth)
 | |
| 			{
 | |
| 				int sep = auth.indexOf(':');
 | |
| 				if(sep != -1)
 | |
| 				{
 | |
| 					String username = auth.substring(0, sep);
 | |
| 					String password = auth.substring(sep + 1);
 | |
| 					if(username == m_Settings.m_aUsername && password == m_Settings.m_aPassword)
 | |
| 						return true;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	response.code = HTTP_STATUS_UNAUTHORIZED;
 | |
| 	response.setHeader("WWW-Authenticate", "Basic realm=\"safeweb\"");
 | |
| 	response.setHeader("401 Wrong credentials", "Authentication required");
 | |
| 	response.setHeader("Connection", "close");
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| void CMain::HttpOnApi(HttpRequest &request, HttpResponse &response)
 | |
| {
 | |
| 	if(!HttpAuthorized(request, response))
 | |
| 		return;
 | |
| 
 | |
| 	String path = request.uri.Path;
 | |
| 	if(path.length() < 6)
 | |
| 	{
 | |
| 		response.code = HTTP_STATUS_NOT_FOUND;
 | |
| 		return;
 | |
| 	}
 | |
| 	String endpoint = path.substring(5);
 | |
| 
 | |
| 	DynamicJsonDocument jsonReq(1024);
 | |
| 	deserializeJson(jsonReq, request.getBody());
 | |
| 	DynamicJsonDocument jsonResp(1024);
 | |
| 
 | |
| 	response.setAllowCrossDomainOrigin("*");
 | |
| 	response.setHeader("Access-Control-Allow-Headers", "Content-Type");
 | |
| 
 | |
| 	if(request.method == HTTP_OPTIONS)
 | |
| 	{
 | |
| 		response.code = HTTP_STATUS_OK;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 
 | |
| 	HttpStatus status = HandleApi(request.method, endpoint, jsonReq, jsonResp);
 | |
| 	if(status != HTTP_STATUS_OK)
 | |
| 	{
 | |
| 		response.code = status;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	String respString;
 | |
| 	serializeJson(jsonResp, respString);
 | |
| 	response.setContentType(MIME_JSON);
 | |
| 	response.sendString(respString);
 | |
| }
 | |
| 
 | |
| HttpStatus CMain::HandleApi(HttpMethod method, String endpoint, JsonDocument &req, JsonDocument &resp)
 | |
| {
 | |
| 	String test;
 | |
| 	serializeJsonPretty(req, test);
 | |
| 
 | |
| 	debugf("request: %s\n", test.c_str());
 | |
| 
 | |
| 	if(endpoint == "dashboard")
 | |
| 	{
 | |
| 		if(method != HTTP_GET) return HTTP_STATUS_METHOD_NOT_ALLOWED;
 | |
| 
 | |
| 		JsonObject lock = resp.createNestedObject("lock");
 | |
| 		lock["locked"] = true;
 | |
| 
 | |
| 		JsonObject door = resp.createNestedObject("door");
 | |
| 		door["closed"] = true;
 | |
| 
 | |
| 		JsonObject battery = resp.createNestedObject("battery");
 | |
| 		debugf("adc: %d\n", system_adc_read());
 | |
| 		battery["voltage"] = (system_adc_read() / 1024.f) / 0.23;
 | |
| 		battery["color"] = "success";
 | |
| 		if(battery["voltage"] < 3.6f)
 | |
| 			battery["color"] = "warning";
 | |
| 
 | |
| 		JsonObject network = resp.createNestedObject("network");
 | |
| 		if(WifiStation.isEnabled())
 | |
| 		{
 | |
| 			network["type"] = "Station";
 | |
| 			network["ssid"] = WifiStation.getSSID();
 | |
| 			network["channel"] = WifiStation.getChannel();
 | |
| 			network["dhcp"] = WifiStation.isEnabledDHCP();
 | |
| 			network["rssi"] = WifiStation.getRssi();
 | |
| 			network["signal"] = Rssi2Quality(WifiStation.getRssi());
 | |
| 			network["address"] = WifiStation.getIP().toString();
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			network["type"] = "Access Point";
 | |
| 			network["ssid"] = WifiAccessPoint.getSSID();
 | |
| 			network["channel"] = WifiStation.getChannel();
 | |
| 			network["dhcp"] = true;
 | |
| 			network["address"] = WifiAccessPoint.getIP().toString();
 | |
| 		}
 | |
| 	}
 | |
| 	else if(endpoint == "unlock")
 | |
| 	{
 | |
| 		if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
 | |
| 
 | |
| 		LockUnlock();
 | |
| 		resp["success"] = "true";
 | |
| 	}
 | |
| 	else if(endpoint == "fingerprint")
 | |
| 	{
 | |
| 		if(method != HTTP_GET) return HTTP_STATUS_METHOD_NOT_ALLOWED;
 | |
| 
 | |
| 		JsonArray fingerprints = resp.createNestedArray("fingerprints");
 | |
| 		uint16_t tmp = Settings().m_FingerPrints.count();
 | |
| 		for(uint16_t i = 0; i < tmp; i++)
 | |
| 		{
 | |
| 			JsonObject obj = fingerprints.createNestedObject();
 | |
| 			const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(i);
 | |
| 
 | |
| 			obj["num"] = finger.m_FingerNum;
 | |
| 			obj["label"] = String(finger.m_aLabel);
 | |
| 
 | |
| 			char aHexDigest[SHA256_SIZE*2+1];
 | |
| 			bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest, sizeof(aHexDigest));
 | |
| 			obj["digest"] = String(aHexDigest);
 | |
| 		}
 | |
| 	}
 | |
| 	else if(endpoint == "fingerprint/label")
 | |
| 	{
 | |
| 		if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
 | |
| 
 | |
| 		if(!req.containsKey("index") || !req.containsKey("label"))
 | |
| 			return HTTP_STATUS_BAD_REQUEST;
 | |
| 
 | |
| 		int index = req["index"].as<int>() - 1;
 | |
| 		String label = req["label"];
 | |
| 
 | |
| 		if(index < 0 || index >= Settings().m_FingerPrints.count())
 | |
| 			return HTTP_STATUS_BAD_REQUEST;
 | |
| 
 | |
| 		CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(index);
 | |
| 		strncpy(finger.m_aLabel, label.c_str(), sizeof(finger.m_aLabel));
 | |
| 		Settings().Save();
 | |
| 
 | |
| 		resp["success"] = true;
 | |
| 	}
 | |
| 	else if(endpoint == "fingerprint/delete")
 | |
| 	{
 | |
| 		if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
 | |
| 
 | |
| 		if(!req.containsKey("index"))
 | |
| 			return HTTP_STATUS_BAD_REQUEST;
 | |
| 
 | |
| 		int index = req["index"].as<int>() - 1;
 | |
| 
 | |
| 		if(index < 0 || index >= Settings().m_FingerPrints.count())
 | |
| 			return HTTP_STATUS_BAD_REQUEST;
 | |
| 
 | |
| 		const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(index);
 | |
| 		FingerPrint().DeleteTemplate(finger.m_FingerNum, 1);
 | |
| 		Settings().m_FingerPrints.removeAt(index);
 | |
| 		Settings().Save();
 | |
| 
 | |
| 		resp["success"] = true;
 | |
| 	}
 | |
| 	else if(endpoint == "fingerprint/enroll")
 | |
| 	{
 | |
| 		if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
 | |
| 
 | |
| 		if(req.containsKey("cancel"))
 | |
| 		{
 | |
| 			if(req["cancel"].as<bool>() && m_Enrolling)
 | |
| 			{
 | |
| 				resp["success"] = FingerLogic().EnrollFinger(false);
 | |
| 				m_Enrolling = false;
 | |
| 				m_Enrolled = false;
 | |
| 			}
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			if(!req.containsKey("enrolling"))
 | |
| 				return HTTP_STATUS_BAD_REQUEST;
 | |
| 
 | |
| 			bool enrolling = req["enrolling"];
 | |
| 			if(enrolling && !m_Enrolling)
 | |
| 			{
 | |
| 				if(m_Enrolled)
 | |
| 				{
 | |
| 					resp["done"] = true;
 | |
| 					resp["message"] = m_EnrollMessage;
 | |
| 					m_Enrolled = false;
 | |
| 				}
 | |
| 				else
 | |
| 				{
 | |
| 					resp["error"] = m_EnrollMessage;
 | |
| 				}
 | |
| 			}
 | |
| 			else if(enrolling && m_Enrolling)
 | |
| 			{
 | |
| 				resp["done"] = false;
 | |
| 				resp["message"] = m_EnrollMessage;
 | |
| 			}
 | |
| 			else // if(!enrolling)
 | |
| 			{
 | |
| 				if(!req.containsKey("label"))
 | |
| 					return HTTP_STATUS_BAD_REQUEST;
 | |
| 
 | |
| 				if(!m_Enrolling)
 | |
| 				{
 | |
| 					FingerLogic().EnrollFinger();
 | |
| 					m_Enrolling = true;
 | |
| 					m_Enrolled = false;
 | |
| 					m_EnrollLabel = String(req["label"].as<const char *>());
 | |
| 					resp["done"] = false;
 | |
| 					m_EnrollMessage = "Started enroll process. Please place your finger on the sensor.";
 | |
| 					resp["message"] = m_EnrollMessage;
 | |
| 				}
 | |
| 				else
 | |
| 				{
 | |
| 					resp["done"] = true;
 | |
| 					m_Enrolled = false;
 | |
| 					resp["message"] = m_EnrollMessage;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		return HTTP_STATUS_NOT_FOUND;
 | |
| 	}
 | |
| 
 | |
| 	return HTTP_STATUS_OK;
 | |
| }
 | |
| 
 | |
| void CMain::HttpOnFile(HttpRequest &request, HttpResponse &response)
 | |
| {
 | |
| 	if(!HttpAuthorized(request, response))
 | |
| 		return;
 | |
| 
 | |
| 	String file = request.uri.Path;
 | |
| 
 | |
| 	if(file == "/")
 | |
| 		file = "index.html";
 | |
| 	if(file[0] == '/')
 | |
| 		file = file.substring(1);
 | |
| 	if(file[0] == '.')
 | |
| 	{
 | |
| 		response.code = HTTP_STATUS_FORBIDDEN;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	response.setCache(86400, true); // It's important to use cache for better performance.
 | |
| 	response.sendFile(file);
 | |
| }
 | |
| 
 | |
| void CMain::OnFingerInterrupt(bool finger)
 | |
| {
 | |
| 	m_FingerLogic.OnFingerInterrupt(finger);
 | |
| }
 | |
| 
 | |
| void CMain::FingerEnable(bool enable)
 | |
| {
 | |
| 	const int pin = FINGER_ENABLE_PIN;
 | |
| 	digitalWrite(pin, !enable);
 | |
| }
 | |
| 
 | |
| void CMain::LockSendBytes(uint8_t *pBytes, uint8_t len)
 | |
| {
 | |
| 	const int pin = SAFELOCK_DATA_PIN;
 | |
| 
 | |
| 	// Init
 | |
| 	digitalWrite(pin, 0);
 | |
| 	delayMicroseconds(10 * 1000);
 | |
| 	digitalWrite(pin, 1);
 | |
| 	delayMicroseconds(10 * 1000);
 | |
| 
 | |
| 	// Send data, calculate checksum and send checksum
 | |
| 	uint8_t chk = 0x00;
 | |
| 	for(uint8_t i = 0; i <= len; i++)
 | |
| 	{
 | |
| 		uint8_t byte;
 | |
| 		if(i == len)
 | |
| 		{
 | |
| 			byte = chk;
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			byte = pBytes[i];
 | |
| 			chk ^= byte;
 | |
| 		}
 | |
| 
 | |
| 		for(int8_t j = 0; j < 8; j++)
 | |
| 		{
 | |
| 			digitalWrite(pin, 0);
 | |
| 			delayMicroseconds(100);
 | |
| 			digitalWrite(pin, 1);
 | |
| 
 | |
| 			uint8_t val = byte & (1 << j);
 | |
| 			if(val)
 | |
| 				delayMicroseconds(300);
 | |
| 			else
 | |
| 				delayMicroseconds(100);
 | |
| 		}
 | |
| 
 | |
| 		digitalWrite(pin, 0);
 | |
| 		delayMicroseconds(100);
 | |
| 		digitalWrite(pin, 1);
 | |
| 		delayMicroseconds(8);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void CMain::LockSendCode(uint8_t pass[8])
 | |
| {
 | |
| 	uint8_t packet[9] = {0x51};
 | |
| 	memcpy(&packet[1], pass, 8);
 | |
| 	LockSendBytes(packet, sizeof(packet));
 | |
| }
 | |
| 
 | |
| void CMain::LockUnlock()
 | |
| {
 | |
| 	LockSendCode(Settings().m_aLockCode);
 | |
| }
 | |
| 
 | |
| void CMain::EnrollMessage(const char *msg, bool error)
 | |
| {
 | |
| 	m_EnrollMessage = msg;
 | |
| 	if(error)
 | |
| 		m_Enrolling = false;
 | |
| }
 | |
| 
 | |
| void CMain::OnFingerVerified(uint16_t fingerNum, uint8_t digest[SHA256_SIZE])
 | |
| {
 | |
| 	int fingerIndex = Settings().m_FingerPrints.indexOf(fingerNum);
 | |
| 	if(fingerIndex == -1)
 | |
| 	{
 | |
| 		debugf("OnFingerVerified: fingerIndex == -1");
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(fingerIndex);
 | |
| 	if(memcmp(digest, finger.m_aDigest, SHA256_SIZE) != 0)
 | |
| 	{
 | |
| 		debugf("OnFingerVerified: SHA256 mismatch");
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	LockUnlock();
 | |
| 
 | |
| 	debugf("OnFingerVerified: OK!!!");
 | |
| }
 | |
| 
 | |
| void CMain::OnFingerEnrolled(uint16_t fingerNum, uint8_t digest[SHA256_SIZE])
 | |
| {
 | |
| 	CSettings::CFingerPrint finger;
 | |
| 	finger.m_FingerNum = fingerNum;
 | |
| 	strncpy(finger.m_aLabel, m_EnrollLabel.c_str(), sizeof(finger.m_aLabel));
 | |
| 	memcpy(finger.m_aDigest, digest, SHA256_SIZE);
 | |
| 
 | |
| 	char aHexDigest[SHA256_SIZE*2+1];
 | |
| 	bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest, sizeof(aHexDigest));
 | |
| 	debugf("OnFingerEnrolled: \"%s\"", aHexDigest);
 | |
| 
 | |
| 	Serial1.printf("(sz: %d) Finger hexdigest: ", sizeof(finger.m_aDigest));
 | |
| 	for(uint8_t i = 0; i < sizeof(finger.m_aDigest); i++)
 | |
| 		Serial1.printf("%x", finger.m_aDigest[i]);
 | |
| 	Serial1.printf("\n");
 | |
| 
 | |
| 	Settings().m_FingerPrints[fingerNum] = finger;
 | |
| 	Settings().Save();
 | |
| 	m_Enrolled = true;
 | |
| 	m_Enrolling = false;
 | |
| 	m_EnrolledFinger = finger;
 | |
| 
 | |
| 	char aBuf[512];
 | |
| 	m_snprintf(aBuf, sizeof(aBuf), "Successfully enrolled new finger \"%s\".", finger.m_aLabel);
 | |
| 	EnrollMessage(aBuf);
 | |
| 
 | |
| 	debugf("OnFingerEnrolled: OK!!!");
 | |
| }
 |