Add a new module to manage Yubikey devices (#785)
* Add a new module to manage a Yubikey device * Set python 3.6+ as a requeriment * remove the enable_shell parameter used by run_through_shell in the yubikey module
This commit is contained in:
parent
d6b9c95a4c
commit
1f8b0a7863
@ -1,7 +1,7 @@
|
|||||||
language: python
|
language: python
|
||||||
sudo: false
|
sudo: false
|
||||||
python:
|
python:
|
||||||
- "3.4"
|
- "3.6"
|
||||||
install:
|
install:
|
||||||
- "pip install -r dev-requirements.txt"
|
- "pip install -r dev-requirements.txt"
|
||||||
script: "./ci-build.sh"
|
script: "./ci-build.sh"
|
||||||
|
@ -7,7 +7,7 @@ i3pystatus
|
|||||||
i3pystatus is a large collection of status modules compatible with i3bar from the i3 window manager.
|
i3pystatus is a large collection of status modules compatible with i3bar from the i3 window manager.
|
||||||
|
|
||||||
:License: MIT
|
:License: MIT
|
||||||
:Python: 3.4+
|
:Python: 3.6+
|
||||||
:Governance: Patches that don't break the build (Travis or docs) are generally just merged. This is a "do-it-yourself" project, so to speak.
|
:Governance: Patches that don't break the build (Travis or docs) are generally just merged. This is a "do-it-yourself" project, so to speak.
|
||||||
:Releases: No further releases are planned. Install it from Git.
|
:Releases: No further releases are planned. Install it from Git.
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ Installation
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
**Supported Python versions**
|
**Supported Python versions**
|
||||||
i3pystatus requires Python 3.4 or newer and is not compatible with
|
i3pystatus requires Python 3.6 or newer and is not compatible with
|
||||||
Python 2.x. Some modules require additional dependencies
|
Python 2.x. Some modules require additional dependencies
|
||||||
documented in the docs.
|
documented in the docs.
|
||||||
|
|
||||||
|
133
i3pystatus/yubikey.py
Normal file
133
i3pystatus/yubikey.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import re
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from i3pystatus import IntervalModule
|
||||||
|
from i3pystatus.core.command import run_through_shell
|
||||||
|
|
||||||
|
|
||||||
|
class Yubikey(IntervalModule):
|
||||||
|
"""
|
||||||
|
This module allows you to lock and unlock your Yubikey in order to avoid
|
||||||
|
the OTP to be triggered accidentally.
|
||||||
|
|
||||||
|
@author Daniel Theodoro <daniel.theodoro AT gmail.com>
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval = 1
|
||||||
|
format = "Yubikey: 🔒"
|
||||||
|
unlocked_format = "Yubikey: 🔓"
|
||||||
|
timeout = 5
|
||||||
|
color = "#00FF00"
|
||||||
|
unlock_color = "#FF0000"
|
||||||
|
|
||||||
|
settings = (
|
||||||
|
("format", "Format string"),
|
||||||
|
("unlocked_format", "Format string when the key is unlocked"),
|
||||||
|
("timeout", "How long the Yubikey will be unlocked (default: 5)"),
|
||||||
|
("color", "Standard color"),
|
||||||
|
("unlock_color", "Set the color used when the Yubikey is unlocked"),
|
||||||
|
)
|
||||||
|
|
||||||
|
on_leftclick = ["set_lock", True]
|
||||||
|
|
||||||
|
find_regex = re.compile(
|
||||||
|
r".*yubikey.*id=(?P<yubid>\d+).*$",
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
status_regex = re.compile(
|
||||||
|
r".*device enabled.*(?P<status>\d)$",
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
lock_file = f"/var/tmp/Yubikey-{os.geteuid()}.lock"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _device_id(self):
|
||||||
|
command = run_through_shell("xinput list")
|
||||||
|
|
||||||
|
rval = ""
|
||||||
|
|
||||||
|
if command.rc == 0:
|
||||||
|
for line in command.out.splitlines():
|
||||||
|
match = self.find_regex.match(line)
|
||||||
|
if match:
|
||||||
|
rval = match.groupdict().get("yubid", "")
|
||||||
|
break
|
||||||
|
|
||||||
|
return rval
|
||||||
|
|
||||||
|
def device_status(self):
|
||||||
|
|
||||||
|
rval = "notfound"
|
||||||
|
|
||||||
|
if not self._device_id:
|
||||||
|
return rval
|
||||||
|
|
||||||
|
result = run_through_shell(f"xinput list-props {self._device_id}")
|
||||||
|
if result.rc == 0:
|
||||||
|
match = self.status_regex.match(result.out.splitlines()[1])
|
||||||
|
if match and "status" in match.groupdict():
|
||||||
|
status = int(match.groupdict()["status"])
|
||||||
|
if status:
|
||||||
|
rval = "unlocked"
|
||||||
|
else:
|
||||||
|
rval = "locked"
|
||||||
|
|
||||||
|
return rval
|
||||||
|
|
||||||
|
def _check_lock(self):
|
||||||
|
try:
|
||||||
|
st = os.stat(self.lock_file)
|
||||||
|
|
||||||
|
if int(time.time() - st.st_ctime) > self.timeout:
|
||||||
|
self.set_lock()
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
self.set_lock()
|
||||||
|
|
||||||
|
def set_lock(self, unlock=False):
|
||||||
|
|
||||||
|
if unlock:
|
||||||
|
command = "enable"
|
||||||
|
else:
|
||||||
|
command = "disable"
|
||||||
|
|
||||||
|
run_through_shell(f"xinput {command} {self._device_id}")
|
||||||
|
open(self.lock_file, mode="w").close()
|
||||||
|
|
||||||
|
def _clear_lock(self):
|
||||||
|
try:
|
||||||
|
os.unlink(self.lock_file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
status = self.device_status()
|
||||||
|
|
||||||
|
if status == "notfound":
|
||||||
|
self._clear_lock()
|
||||||
|
self.output = {
|
||||||
|
"full_text": "",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if status == "unlocked":
|
||||||
|
self.output = {
|
||||||
|
"full_text": self.unlocked_format,
|
||||||
|
"color": self.unlock_color
|
||||||
|
}
|
||||||
|
self._check_lock()
|
||||||
|
|
||||||
|
elif status == "locked":
|
||||||
|
self.output = {
|
||||||
|
"full_text": self.format,
|
||||||
|
"color": self.color
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self.output = {
|
||||||
|
"full_text": f"Error: {status}",
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user