Access Control
Implement
Server Side
- OTP Issuer
- BLE Advertiser
- GPIO Controller
- HTTP Server
- Service Broadcast
- Slack Incoming Hook
Server Architecture
Issue OTP
GPIO
Wired
HTTP Hook
Handle Request
Service Broadcast
Bluetooth Advertiser
const bleno = require('@abandonware/bleno');
const authenticator = require('authenticator');
const OTP_SECRET = process.env.OTP_SECRET || 'vi7g 2dk6 ckdq w6of m6bq bng6 a5wv nmhk';
const PROFILE_NAME = 'RytassProfile';
const RYTASS_UUID = '1101a98bbc854d679c862fd9a6bd0c3c';
const OTP_UUID = '2f151c41713748f6a6b2feb9dd1d2c51';
const OTPCharacteristic = new bleno.Characteristic({
uuid: OTP_UUID,
properties: ['read'],
onReadRequest: (offset, callback) => {
const code = authenticator.generateToken(OTP_SECRET);
console.log('On Read', code);
callback(bleno.Characteristic.RESULT_SUCCESS, Buffer.from(code));
},
onSubscribe: (maxValueSize, callback) => {
console.log('On Subscribe', maxValueSize);
},
});
const RytassService = new bleno.PrimaryService({
uuid: RYTASS_UUID,
characteristics: [OTPCharacteristic],
});
bleno.on('stateChange', (state) => {
console.log('State Changed:', state);
bleno.startAdvertising(PROFILE_NAME, [RYTASS_UUID]);
});
bleno.on('advertisingStart', () => {
console.log('Advertising Start');
bleno.setServices([RytassService]);
});
HTTP Server
const Koa = require('koa');
const koaBody = require('koa-body');
const Router = require('@koa/router');
const PORT = Number(process.env.PORT || '9051');
const app = new Koa();
const router = new Router({ prefix: '/gates' });
router.post('/:action', async (ctx) => {
const action = ctx.params.action;
const code = ctx.request.body.code;
if (!~['open', 'close'].indexOf(action)) return;
if (!authenticator.verifyToken(OTP_SECRET, code)) {
ctx.status = 400;
ctx.body = { message: 'Invalid Request' };
return;
}
// Open or Close Door
ctx.status = 204;
});
app.use(koaBody({
urlencoded: false,
text: false,
}));
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(PORT, () => {
console.log(`Gate Server Listen on ${PORT}`);
});
GPIO Handler
const { Gpio } = require('onoff');
const OPEN_PIN = process.env.OPENPIN || '23';
const CLOSE_PIN = process.env.CLOSEPIN || '22';
function gpioHandler(action) {
const pinCode = action === 'open' ? OPEN_PIN : CLOSE_PIN;
const pin = new Gpio(pinCode, 'out');
pin.writeSync(0);
setTimeout(() => pin.writeSync(1), 1000);
}
Service Broadcast
const bonjour = require('bonjour')();
const PORT = Number(process.env.PORT || '9051');
bonjour.publish({
name: 'RytassGateController',
type: 'http',
protocol: 'tcp',
port: PORT,
});
Slack Integration
const axios = require('axios');
const SLACK_HOOK = process.env.SLACK_HOOK_URL || 'https://hooks.slack.com/services/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';
function notifySlack(action) {
axios(SLACK_HOOK, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
data: JSON.stringify({
text: `Rytass HQ Door ${action === 'open' ? 'Opened' : 'Closed'}`,
}),
});
}
Client Side
- BLE Peripherals Scanner
- Service Discover
- Characteristic Discover
- Characteristic Reader
- HTTP Request
- Network Service Finder
Story
BLE Scanner
import CoreBluetooth
let MDNS_NAME = "RytassGateController";
let GATE_SERVICE_UUID = CBUUID(string: "1101A98B-BC85-4D67-9C86-2FD9A6BD0C3C");
let OTP_CHAR_UUID = CBUUID(string: "2F151C41-7137-48F6-A6B2-FEB9DD1D2C51");
class ViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {
var centralManager: CBCentralManager?;
var gatePeripheral: CBPeripheral?;
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .unknown:
print("State Unknown");
case .resetting:
print("Staet Resetting");
case .unsupported:
print("Unsupport");
case .unauthorized:
print("Unauthorized");
case .poweredOff:
print("Power Off");
openBtn.isEnabled = false;
closeBtn.isEnabled = false;
case .poweredOn:
print("Power On");
openBtn.isEnabled = true;
closeBtn.isEnabled = true;
@unknown default:
print("Invalid State");
}
}
}
BLE Scanner
override func viewDidLoad() {
super.viewDidLoad()
centralManager = CBCentralManager(delegate: self, queue: nil);
}
@IBAction func onOpenClick(_ sender: Any) {
print("Click Open");
requestAction = "open";
centralManager?.scanForPeripherals(withServices: [GATE_SERVICE_UUID], options: nil);
openBtn.isEnabled = false;
closeBtn.isEnabled = false;
}
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber
) {
print(advertisementData);
gatePeripheral = peripheral;
centralManager?.stopScan();
centralManager?.connect(gatePeripheral!, options: nil);
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("Connected!");
gatePeripheral?.delegate = self;
gatePeripheral?.discoverServices([GATE_SERVICE_UUID]);
}
BLE Scanner
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
let targetService = peripheral.services?.first(where: { $0.uuid.uuidString == GATE_SERVICE_UUID.uuidString });
print("Services Responsed!");
if (targetService != nil) {
print("Found Service:", targetService!);
gatePeripheral?.discoverCharacteristics(nil, for: targetService!);
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
let otpCharacteristic = service.characteristics?.first(where: { $0.uuid.uuidString == OTP_CHAR_UUID.uuidString });
print("Characteristics Responsed!")
if (otpCharacteristic != nil) {
print("Found Characteristics", otpCharacteristic!);
peripheral.readValue(for: otpCharacteristic!);
}
}
Discover services and characteristics
BLE Scanner
let TARGET_HOST = "http://192.168.2.159:9051";
var requestAction: String = "open";
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
print("Read Value", characteristic);
let otpCode = String(bytes: characteristic.value!, encoding: .utf8);
if (otpCode != nil) {
print("Got OTP", otpCode!, "Action:", requestAction);
var request = URLRequest(url: URL(string: "\(TARGET_HOST)/gates/\(requestAction)")!);
request.httpMethod = "POST";
request.setValue("application/json", forHTTPHeaderField: "Content-Type");
request.httpBody = try? JSONSerialization.data(withJSONObject: ["code": otpCode], options: []);
URLSession.shared.dataTask(with: request) { (data, response, error) in
if response != nil {
print(response!);
}
}.resume();
}
centralManager?.cancelPeripheralConnection(gatePeripheral!);
}
Open / Close Request
BLE Scanner
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
print("Disconnected!");
gatePeripheral = nil;
openBtn.isEnabled = true;
closeBtn.isEnabled = true;
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
print("Connect Failed");
gatePeripheral = nil;
openBtn.isEnabled = true;
closeBtn.isEnabled = true;
}
Error Handler
Service Checker
let browser = NetServiceBrowser.init();
let monitor = NWPathMonitor();
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
if (service.name == MDNS_NAME) {
print("LAN found!");
openBtn.isEnabled = true;
closeBtn.isEnabled = true;
warningLabel.isHidden = true;
browser.stop();
}
}
override func viewDidLoad() {
super.viewDidLoad()
monitor.pathUpdateHandler = { path in
print("Network changed");
self.browser.stop();
DispatchQueue.main.async {
self.openBtn.isEnabled = false;
self.closeBtn.isEnabled = false;
self.warningLabel.isHidden = false;
self.browser.searchForServices(ofType: "_http._tcp.", inDomain: "local.");
}
}
monitor.start(queue: DispatchQueue.global());
}
ขอบคุณ
Rytass Authenticator
By Chia Yu Pai
Rytass Authenticator
- 332