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

  • 323