Compare commits

..

13 Commits

9 changed files with 274 additions and 52 deletions

201
XWBSSOi.py Normal file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
import ctypes
import ctypes.wintypes
import winreg
import contextlib
from typing import Any, Optional, Generator
DEFAULT_USER_AGENT = 'Borland SOAP 1.2'
DEFAULT_ISSUER = 'https://ssoi.sts.va.gov/Issuer/smtoken/SAML2'
HCERTSTORE = ctypes.c_void_p
PCERT_INFO = ctypes.c_void_p
HCRYPTPROV_LEGACY = ctypes.c_void_p
CERT_STORE_PROV_MEMORY = b'Memory'
X509_ASN_ENCODING = 0x00000001
PKCS_7_ASN_ENCODING = 0x00010000
CERT_COMPARE_ANY = 0
CERT_COMPARE_SHIFT = 16
CERT_FIND_ANY = CERT_COMPARE_ANY<<CERT_COMPARE_SHIFT
CERT_NAME_FRIENDLY_DISPLAY_TYPE = 5
CERT_DIGITAL_SIGNATURE_KEY_USAGE = 0x80
CERT_STORE_ADD_ALWAYS = 4
CERT_HASH_PROP_ID = CERT_SHA1_HASH_PROP_ID = 3
class CERT_CONTEXT(ctypes.Structure):
_fields_ = [
('dwCertEncodingType', ctypes.wintypes.DWORD),
('pbCertEncoded', ctypes.POINTER(ctypes.wintypes.BYTE)),
('cbCertEncoded', ctypes.wintypes.DWORD),
('pCertInfo', PCERT_INFO),
('hCertStore', HCERTSTORE),
]
PCCERT_CONTEXT = ctypes.POINTER(CERT_CONTEXT)
crypt32 = ctypes.WinDLL('crypt32')
CertOpenStore = crypt32.CertOpenStore
CertOpenStore.restype = HCERTSTORE
CertOpenStore.argtypes = (ctypes.wintypes.LPCSTR, ctypes.wintypes.DWORD, HCRYPTPROV_LEGACY, ctypes.wintypes.DWORD, ctypes.c_void_p)
CertOpenSystemStoreW = crypt32.CertOpenSystemStoreW
CertOpenSystemStoreW.restype = HCERTSTORE
CertOpenSystemStoreW.argtypes = (HCRYPTPROV_LEGACY, ctypes.wintypes.LPCWSTR)
CertCloseStore = crypt32.CertCloseStore
CertCloseStore.restype = ctypes.wintypes.BOOL
CertCloseStore.argtypes = (HCERTSTORE, ctypes.wintypes.DWORD)
CertEnumCertificatesInStore = crypt32.CertEnumCertificatesInStore
CertEnumCertificatesInStore.restype = PCCERT_CONTEXT
CertEnumCertificatesInStore.argtypes = (HCERTSTORE, PCCERT_CONTEXT)
CertFindCertificateInStore = crypt32.CertFindCertificateInStore
CertFindCertificateInStore.restype = PCCERT_CONTEXT
CertFindCertificateInStore.argtypes = (HCERTSTORE, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_void_p, PCCERT_CONTEXT)
CertAddCertificateContextToStore = crypt32.CertAddCertificateContextToStore
CertAddCertificateContextToStore.restype = ctypes.wintypes.BOOL
CertAddCertificateContextToStore.argtypes = (HCERTSTORE, PCCERT_CONTEXT, ctypes.wintypes.DWORD, ctypes.POINTER(PCCERT_CONTEXT))
CertGetNameStringW = crypt32.CertGetNameStringW
CertGetNameStringW.restype = ctypes.wintypes.DWORD
CertGetNameStringW.argtypes = (PCCERT_CONTEXT, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_void_p, ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD)
CertGetIntendedKeyUsage = crypt32.CertGetIntendedKeyUsage
CertGetIntendedKeyUsage.restype = ctypes.wintypes.BOOL
CertGetIntendedKeyUsage.argtypes = (ctypes.wintypes.DWORD, PCERT_INFO, ctypes.POINTER(ctypes.wintypes.BYTE), ctypes.wintypes.DWORD)
CertGetCertificateContextProperty = crypt32.CertGetCertificateContextProperty
CertGetCertificateContextProperty.restype = ctypes.wintypes.BOOL
CertGetCertificateContextProperty.argtypes = (PCCERT_CONTEXT, ctypes.wintypes.DWORD, ctypes.c_void_p, ctypes.POINTER(ctypes.wintypes.DWORD))
CertVerifyTimeValidity = crypt32.CertVerifyTimeValidity
CertVerifyTimeValidity.restype = ctypes.wintypes.LONG
CertVerifyTimeValidity.argtypes = (ctypes.wintypes.LPFILETIME, PCERT_INFO)
cryptui = ctypes.WinDLL('cryptui')
CryptUIDlgSelectCertificateFromStore = cryptui.CryptUIDlgSelectCertificateFromStore
CryptUIDlgSelectCertificateFromStore.restype = PCCERT_CONTEXT
CryptUIDlgSelectCertificateFromStore.argtypes = (HCERTSTORE, ctypes.wintypes.HWND, ctypes.wintypes.LPCWSTR, ctypes.wintypes.LPCWSTR, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_void_p)
GetConsoleWindow = ctypes.windll.kernel32.GetConsoleWindow
GetConsoleWindow.restype = ctypes.wintypes.HWND
@contextlib.contextmanager
def ManagedCertOpenStore(lpszStoreProvider: ctypes.wintypes.LPCSTR, dwEncodingType: ctypes.wintypes.DWORD, hCryptProv: HCRYPTPROV_LEGACY, dwFlags: ctypes.wintypes.DWORD, pvPara: ctypes.c_void_p) -> Generator[HCERTSTORE, None, None]:
res = CertOpenStore(lpszStoreProvider, dwEncodingType, hCryptProv, dwFlags, pvPara)
try:
yield res
finally:
CertCloseStore(res, 0)
@contextlib.contextmanager
def ManagedCertOpenSystemStore(hProv: HCRYPTPROV_LEGACY, szSubsystemProtocol: ctypes.wintypes.LPCWSTR) -> Generator[HCERTSTORE, None, None]:
res = CertOpenSystemStoreW(hProv, szSubsystemProtocol)
try:
yield res
finally:
CertCloseStore(res, 0)
def get_vista_certificate(show_cert_dialog: bool=True, hwnd: Optional[int]=0) -> PCCERT_CONTEXT:
with ManagedCertOpenSystemStore(0, 'MY') as store_system, ManagedCertOpenStore(CERT_STORE_PROV_MEMORY, 0, None, 0, None) as store_memory:
cert_selection = cert_iter = None
while cert_iter := CertEnumCertificatesInStore(store_system, cert_iter):
if cert_valid := CertFindCertificateInStore(store_system, X509_ASN_ENCODING|PKCS_7_ASN_ENCODING, 0, CERT_FIND_ANY, None, None):
name_bufsz = CertGetNameStringW(cert_iter, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, None, None, 0)
buf = ctypes.create_unicode_buffer(name_bufsz)
CertGetNameStringW(cert_iter, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, None, buf, name_bufsz)
name_string = buf.value
key_usage_bits = ctypes.wintypes.BYTE()
CertGetIntendedKeyUsage(X509_ASN_ENCODING|PKCS_7_ASN_ENCODING, cert_iter.contents.pCertInfo, ctypes.byref(key_usage_bits), ctypes.sizeof(key_usage_bits))
valid_date = CertVerifyTimeValidity(None, cert_iter.contents.pCertInfo)
if ((key_usage_bits.value&CERT_DIGITAL_SIGNATURE_KEY_USAGE) == CERT_DIGITAL_SIGNATURE_KEY_USAGE) and (valid_date == 0) and ('Card Authentication' not in name_string) and ('0,' not in name_string) and ('Signature' not in name_string):
CertAddCertificateContextToStore(store_memory, cert_iter, CERT_STORE_ADD_ALWAYS, ctypes.byref(cert_valid))
cert_selection = cert_valid
return CryptUIDlgSelectCertificateFromStore(store_memory, hwnd if hwnd is not None else GetConsoleWindow(), 'VistA Logon - Certificate Selection', 'Select a certificate for VistA authentication', 0, 0, None) if show_cert_dialog else cert_selection
def get_certificate_thumbprint(certificate: PCCERT_CONTEXT) -> str:
bufsz = ctypes.wintypes.DWORD()
CertGetCertificateContextProperty(certificate, CERT_HASH_PROP_ID, None, ctypes.byref(bufsz))
buffer = ctypes.create_string_buffer(bufsz.value)
CertGetCertificateContextProperty(certificate, CERT_HASH_PROP_ID, buffer, ctypes.byref(bufsz))
return buffer.value
def get_certificate_friendly_display_name(certificate: PCCERT_CONTEXT) -> str:
name_bufsz = CertGetNameStringW(certificate, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, None, None, 0)
buffer = ctypes.create_unicode_buffer(name_bufsz)
CertGetNameStringW(certificate, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, None, buffer, name_bufsz)
return buffer.value
# WS-Trust STS endpoints from VDL documentation
# https://www.va.gov/vdl/documents/Infrastructure/KAAJEE/kaajee_ssowap_8_791_depg_r.pdf
# https://www.va.gov/vdl/documents/Financial_Admin/Bed_Management_Solution_(BMS)/bms_4_0_tm.pdf
# https://www.va.gov/vdl/documents/VistA_GUI_Hybrids/National_Utilization_Management_Integration_Archive/numi_server_setup_guide_v15_9.pdf
get_registry_iam = lambda: get_registry_value(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\IAM', default='https://services.eauth.va.gov:9301/STS/RequestSecurityToken')
get_registry_iam_ad = lambda: get_registry_value(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\IAM_AD', default='https://services.eauth.va.gov:9201/STS/RequestSecurityToken')
get_registry_rioserver = lambda: get_registry_value(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\RIOSERVER', default='SecurityTokenService')
get_registry_rioport = lambda: get_registry_value(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\RIOPORT', default='RequestSecurityToken')
def get_registry_value(hkey: int, subkey: str, value: Optional[str]=None, default: Any=None) -> Any:
try:
with winreg.OpenKey(hkey, subkey) as key:
return winreg.QueryValueEx(key, value)[0]
except FileNotFoundError:
return default
def get_iam_request(application: str, issuer: str) -> str:
return f'''<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
<soapenv:Header/>
<soapenv:Body>
<ns:RequestSecurityToken>
<ns:Base>
<wss:TLS xmlns:wss="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"/>
</ns:Base>
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<wsa:EndpointReference xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<wsa:Address>{application}</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<ns:Issuer>
<wsa:Address xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">{issuer}</wsa:Address>
</ns:Issuer>
<ns:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Validate</ns:RequestType>
</ns:RequestSecurityToken>
</soapenv:Body>
</soapenv:Envelope>'''
def get_local_computer_name() -> str:
import socket
return socket.getfqdn()
def get_app_name() -> str:
import sys, os
return os.path.basename(sys.argv[0])
def get_sso_token(iam: Optional[str]=None, ua: Optional[str]=None, certificate: Optional[str]=None, issuer: Optional[str]=None, hostname: Optional[str]=None, application: Optional[str]=None) -> str:
import sys, subprocess
if certificate is None:
if choice := get_vista_certificate():
certificate = get_certificate_thumbprint(choice).hex()
if certificate is not None:
res = subprocess.run(['curl', '-fsSL', '-X', 'POST', iam or get_registry_iam(), '--ca-native', '--cert', 'CurrentUser\\MY\\' + certificate, '-A', ua or DEFAULT_USER_AGENT, '-H', 'Content-Type: application/xml', '-H', 'Accept: application/xml', '-d', get_iam_request(f"https://{hostname or get_local_computer_name()}/Delphi_RPC_Broker/{application or get_app_name()}", issuer or DEFAULT_ISSUER)], capture_output=True)
print(res.stderr.decode('utf8'), end='', file=sys.stderr)
return res.stdout.decode('utf8')
async def get_sso_token_async(iam: Optional[str]=None, ua: Optional[str]=None, certificate: Optional[str]=None, issuer: Optional[str]=None, hostname: Optional[str]=None, application: Optional[str]=None) -> str:
import sys, asyncio
if certificate is None:
certificate = get_certificate_thumbprint(get_vista_certificate()).hex()
res = await (await asyncio.create_subprocess_exec('curl', '-fsSL', '-X', 'POST', iam or get_registry_iam(), '--ca-native', '--cert', 'CurrentUser\\MY\\' + certificate, '-A', ua or DEFAULT_USER_AGENT, '-H', 'Content-Type: application/xml', '-H', 'Accept: application/xml', '-d', get_iam_request(f"https://{hostname or get_local_computer_name()}/Delphi_RPC_Broker/{application or get_app_name()}", issuer or DEFAULT_ISSUER), stdin=None, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)).communicate()
print(res[1].decode('utf8'), end='', file=sys.stderr)
return res[0].decode('utf8')
if __name__ == '__main__':
try:
print(get_sso_token())
except OSError:
exit(1)

17
auth.py
View File

@ -1,17 +0,0 @@
#!/usr/bin/env python3
import ctypes
# Load DLL
XUIAMSSOi = ctypes.WinDLL('C:\\Program Files (x86)\\Micro Focus\\Reflection\\XUIAMSSOi.dll')
XUIAMSSOi.MySsoTokenVBA.restype = ctypes.c_long
XUIAMSSOi.MySsoTokenVBA.argtypes = (ctypes.c_wchar_p, ctypes.c_long)
# Authenticate against smartcard
def XUIAMSSOi_MySsoTokenVBA(bufsize=15000):
buf = ctypes.create_unicode_buffer(bufsize)
sz = XUIAMSSOi.MySsoTokenVBA(buf, bufsize)
if sz <= bufsize:
return buf.value.encode('utf-16')[2:].decode('latin-1')
else:
return XUIAMSSOi_MySsoTokenVBA(sz)

View File

@ -53,8 +53,8 @@
} }
function timeshift_month(date, diff) { function timeshift_month(date, diff) {
var month = date.getMonth() + diff; var month = date.getMonth() + diff, month_mod12 = month%12;
return new Date(date.getFullYear() + Math.floor(month/12), month >= 0 ? (month%12) : (month%12 + 12), date.getDate()); return new Date(date.getFullYear() + Math.floor(month/12), month_mod12 >= 0 ? (month_mod12) : (month_mod12 + 12), date.getDate());
} }
function datecalc(date, range, direction) { function datecalc(date, range, direction) {

View File

@ -205,12 +205,27 @@
id: 'OR_PN:' + time.getTime() + ':' + x[2], id: 'OR_PN:' + time.getTime() + ':' + x[2],
emblem: 'emblem-notes', emblem: 'emblem-notes',
title: [x[4], x[5], '#' + x[2]], title: [x[4], x[5], '#' + x[2]],
detail: escape_html(x[6]) detail: escape_html(collapse_lines(x[6]))
}; };
}), }),
loader: reportloader_chunk, loader: reportloader_chunk,
enabled: true enabled: true
}, },
{
name: 'Discharge',
rpt_id: 'OR_DS:DISCHARGE SUMMARY~TIUDCS;ORDV04;57;',
map: flow(f_parse_columns, function(x) {
var time = new Date(x[3]);
return {
time,
id: 'OR_DS:' + time.getTime() + ':' + x[2],
emblem: 'emblem-notes',
title: ['DISCHARGE SUMMARY', x[4], x[2], x[3]],
detail: escape_html(collapse_lines(x[7]))
};
}),
loader: reportloader_chunk
},
{ {
name: 'Labs', name: 'Labs',
rpt_id: 'OR_OV_R:LAB OVERVIEW (COLLECTED SPECIMENS)~OV;ORDV02C;32;', rpt_id: 'OR_OV_R:LAB OVERVIEW (COLLECTED SPECIMENS)~OV;ORDV02C;32;',
@ -224,7 +239,7 @@
detail: escape_html(x[15]) detail: escape_html(x[15])
}; };
}), }),
loader: reportloader_alpha loader: reportloader_chunk
}, },
{ {
name: 'Microbiology', name: 'Microbiology',
@ -412,6 +427,10 @@
return fn; return fn;
} }
function collapse_lines(s) {
return s.replace(/(\S)[^\S\r\n]+\r?\n([^\s\.,\/#!$%\^&\*;:=\-_`~])/g, '$1 $2').replace(/([\.,!;])\r?\n([^\s\.,\/#!$%\^&\*;:=\-_`~])/g, '$1 $2');
}
const escape_div = document.createElement('div'); const escape_div = document.createElement('div');
function escape_html(s) { function escape_html(s) {
escape_div.textContent = s; escape_div.textContent = s;

View File

@ -177,7 +177,7 @@
if((satisfied) && (updated)) { if((satisfied) && (updated)) {
item = calculation.calc(...calculation.deps.map(x => history[x].value), history[calculation.name] && history[calculation.name].value); item = calculation.calc(...calculation.deps.map(x => history[x].value), history[calculation.name] && history[calculation.name].value);
if((item !== undefined) && (item !== null) && (item === item) && (item != 'NaN')) { // item === item if not NaN if((item !== undefined) && (item !== null) && (item === item) && (item != 'NaN')) { // item === item if not NaN
results.push(history[calculation.name] = update[calculation.name] = Object.assign({ time: group.key, value: item }, calculation)); results.push(history[calculation.name] = update[calculation.name] = item = Object.assign({ time: group.key, value: item }, calculation));
if((item.hasOwnProperty('rangeL')) && (item.value < item.rangeL)) item.flag = 'L'; if((item.hasOwnProperty('rangeL')) && (item.value < item.rangeL)) item.flag = 'L';
else if((item.hasOwnProperty('rangeH')) && (item.value > item.rangeH)) item.flag = 'H'; else if((item.hasOwnProperty('rangeH')) && (item.value > item.rangeH)) item.flag = 'H';
} }
@ -226,7 +226,7 @@
}, },
watch: { watch: {
async resultset(value) { async resultset(value) {
this.$nextTick(() => this.$refs.headers ? this.$refs.headers.scrollIntoView({ block: 'nearest', inline: 'end' }) : null); this.$nextTick(() => (this.$refs.headers) && (this.$refs.headers.length > 0) ? this.$refs.headers[this.$refs.headers.length - 1].scrollIntoView({ block: 'nearest', inline: 'end' }) : null);
} }
}, },
methods: { methods: {

View File

@ -22,7 +22,9 @@
{ name: 'BMI', unit: 'kg/m²', rangeL: 18.5, rangeH: 24.9, range: '18.5 - 24.9', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (10000*Wt/(Ht*Ht)).toPrecision(3) }, { name: 'BMI', unit: 'kg/m²', rangeL: 18.5, rangeH: 24.9, range: '18.5 - 24.9', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (10000*Wt/(Ht*Ht)).toPrecision(3) },
{ name: 'BSA', unit: 'm²', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (0.007184*Math.pow(Ht, 0.725)*Math.pow(Wt, 0.425)).toPrecision(3) }, { name: 'BSA', unit: 'm²', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (0.007184*Math.pow(Ht, 0.725)*Math.pow(Wt, 0.425)).toPrecision(3) },
{ name: 'CrCl', unit: 'mL/min', deps: ['Age', 'Sex', 'Wt', 'CREATININE'], calc: (Age, Sex, Wt, CREATININE) => (((140 - Age) * Wt)/(72*CREATININE)*(Sex == 'M' ? 1 : 0.85)).toPrecision(4) }, { name: 'CrCl', unit: 'mL/min', deps: ['Age', 'Sex', 'Wt', 'CREATININE'], calc: (Age, Sex, Wt, CREATININE) => (((140 - Age) * Wt)/(72*CREATININE)*(Sex == 'M' ? 1 : 0.85)).toPrecision(4) },
{ name: 'RETICYLOCYTE#', unit: 'K/cmm', rangeL: 50, rangeH: 100, range: '50 - 100', deps: ['RBC', 'RETICULOCYTES'], calc: (RBC, RETICULOCYTES) => (10*RBC*RETICULOCYTES).toPrecision(3) } { name: 'RETICYLOCYTE#', unit: 'K/cmm', rangeL: 50, rangeH: 100, range: '50 - 100', deps: ['RBC', 'RETICULOCYTES'], calc: (RBC, RETICULOCYTES) => (10*RBC*RETICULOCYTES).toPrecision(3) },
{ name: 'CALCIUM CORRECTED', unit: 'mg/dL', rangeL: 8.9, rangeH: 10.3, range: '8.9 - 10.3', deps: ['CALCIUM', 'ALBUMIN'], calc: (CALCIUM, ALBUMIN) => ALBUMIN < 4 ? (+CALCIUM + 0.8*(4 - ALBUMIN)).toPrecision(3) : undefined },
{ name: 'IRON SATURATION', unit: '%', rangeL: 15, rangeH: 55, range: '15 - 55', comment: 'IRON/TIBC', deps: ['IRON', 'TIBC'], calc: (IRON, TIBC) => (100*IRON/TIBC).toPrecision(3) },
]; ];
const reports = [ const reports = [
@ -30,9 +32,9 @@
{ name: 'CBC', value: ['HGB', 'MCV', 'RETICYLOCYTE#', 'PLT', 'WBC', 'NEUTROPHIL#'], selected: false }, { name: 'CBC', value: ['HGB', 'MCV', 'RETICYLOCYTE#', 'PLT', 'WBC', 'NEUTROPHIL#'], selected: false },
{ name: 'Renal', value: ['CREATININE', 'UREA NITROGEN', 'EGFR CKD-EPI 2021', 'Estimated GFR dc\'d 3/30/2022', 'CrCl'], selected: false }, { name: 'Renal', value: ['CREATININE', 'UREA NITROGEN', 'EGFR CKD-EPI 2021', 'Estimated GFR dc\'d 3/30/2022', 'CrCl'], selected: false },
{ name: 'Hepatic', value: ['SGOT', 'SGPT', 'LDH', 'ALKALINE PHOSPHATASE', 'GAMMA-GTP', 'TOT. BILIRUBIN', 'DIR. BILIRUBIN', 'ALBUMIN'], selected: false }, { name: 'Hepatic', value: ['SGOT', 'SGPT', 'LDH', 'ALKALINE PHOSPHATASE', 'GAMMA-GTP', 'TOT. BILIRUBIN', 'DIR. BILIRUBIN', 'ALBUMIN'], selected: false },
{ name: 'Electrolytes', value: ['SODIUM', 'CHLORIDE', 'CO2', 'CALCIUM', 'IONIZED CALCIUM (LABCORP)', 'POTASSIUM', 'MAGNESIUM', 'PO4', 'ANION GAP', 'OSMOBLD'], selected: false }, { name: 'Electrolytes', value: ['SODIUM', 'CHLORIDE', 'CO2', 'CALCIUM', 'CALCIUM CORRECTED', 'IONIZED CALCIUM (LABCORP)', 'POTASSIUM', 'MAGNESIUM', 'PO4', 'ANION GAP', 'OSMOBLD'], selected: false },
{ name: 'Coagulation', value: ['PT', 'INR', 'PTT'], selected: false }, { name: 'Coagulation', value: ['PT', 'INR', 'PTT'], selected: false },
{ name: 'Vitamins', value: ['FERRITIN', 'IRON', 'TIBC', 'B 12', 'FOLATE', 'VITAMIN D TOTAL 25-OH'], selected: false }, { name: 'Vitamins', value: ['FERRITIN', 'IRON', 'TIBC', 'IRON SATURATION', 'B 12', 'FOLATE', 'VITAMIN D TOTAL 25-OH'], selected: false },
{ name: 'Thyroid', value: ['TSH', 'T4 (THYROXINE)'], selected: false }, { name: 'Thyroid', value: ['TSH', 'T4 (THYROXINE)'], selected: false },
{ name: 'Myeloma', value: ['PROTEIN,TOT SER (LC)', 'ALBUMIN [for SPEP](LC)', 'ALPHA-1 GLOBULIN S (LC)', 'ALPHA-2 GLOBULIN S (LC)', 'BETA GLOBULIN S (LC)', 'GAMMA GLOBULIN S (LC)', 'GLOBULIN,TOTAL S (LC)', 'A/G RATIO S (LC)', 'M-SPIKE S (LC)', 'IMMUNOFIXATION SERUM (LC)', 'FREE KAPPA LT CHAIN, S (LC)', 'FREE LAMBDA LT CHAIN, S (LC)', 'KAPPA/LAMBDA RATIO, S (LC)', 'KLRATIO', 'IMMUNOGLOBULIN G,QN (LC)', 'IMMUNOGLOBULIN A,QN (LC)', 'IMMUNOGLOBULIN M,QN (LC)', 'IGG', 'IGA', 'IGM', 'ALBUMIN [for RAND UR](LC):U', 'ALPHA-1 GLOB RAND UR(LC):U', 'ALPHA-2 GLOB RAND UR(LC):U', 'BETA GLOB RAND UR(LC):U', 'GAMMA GLOB RAND UR(LC):U', 'M-SPIKE% RAND UR(LC):U', 'PROTEIN,TOT UR(LC):U', 'FKLCUR:U', 'FLLCUR:U', 'KAPPA/LAMBDA RATIO, UR (LC):U', 'KLRATIO:U', 'PROTEIN,24H CALC(LC):U', 'ALBUMIN [for 24UPEP](LC):U', 'ALPHA-1 GLOBULIN 24H(LC):U', 'ALPHA-2 GLOBULIN 24H(LC):U', 'BETA GLOBULIN 24H(LC):U', 'GAMMA GLOBULIN 24H(LC):U', 'M-SPIKE% 24H(LC):U', 'M-SPIKE mg/24hr(LC):U', 'FR KAPPA LTCH:U', 'FR LAMBDA LTCH:U'], selected: false } { name: 'Myeloma', value: ['PROTEIN,TOT SER (LC)', 'ALBUMIN [for SPEP](LC)', 'ALPHA-1 GLOBULIN S (LC)', 'ALPHA-2 GLOBULIN S (LC)', 'BETA GLOBULIN S (LC)', 'GAMMA GLOBULIN S (LC)', 'GLOBULIN,TOTAL S (LC)', 'A/G RATIO S (LC)', 'M-SPIKE S (LC)', 'IMMUNOFIXATION SERUM (LC)', 'FREE KAPPA LT CHAIN, S (LC)', 'FREE LAMBDA LT CHAIN, S (LC)', 'KAPPA/LAMBDA RATIO, S (LC)', 'KLRATIO', 'IMMUNOGLOBULIN G,QN (LC)', 'IMMUNOGLOBULIN A,QN (LC)', 'IMMUNOGLOBULIN M,QN (LC)', 'IGG', 'IGA', 'IGM', 'ALBUMIN [for RAND UR](LC):U', 'ALPHA-1 GLOB RAND UR(LC):U', 'ALPHA-2 GLOB RAND UR(LC):U', 'BETA GLOB RAND UR(LC):U', 'GAMMA GLOB RAND UR(LC):U', 'M-SPIKE% RAND UR(LC):U', 'PROTEIN,TOT UR(LC):U', 'FKLCUR:U', 'FLLCUR:U', 'KAPPA/LAMBDA RATIO, UR (LC):U', 'KLRATIO:U', 'PROTEIN,24H CALC(LC):U', 'ALBUMIN [for 24UPEP](LC):U', 'ALPHA-1 GLOBULIN 24H(LC):U', 'ALPHA-2 GLOBULIN 24H(LC):U', 'BETA GLOBULIN 24H(LC):U', 'GAMMA GLOBULIN 24H(LC):U', 'M-SPIKE% 24H(LC):U', 'M-SPIKE mg/24hr(LC):U', 'FR KAPPA LTCH:U', 'FR LAMBDA LTCH:U'], selected: false }
]; ];
@ -47,13 +49,14 @@
function vitals_normalize(rs) { function vitals_normalize(rs) {
return rs.map(function(x) { return rs.map(function(x) {
var comment = x.comment && x.comment.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
var res = { var res = {
time: x.datetime, time: x.datetime,
name: x.name, name: x.name,
unit: x.unit, unit: x.unit,
value: x.value, value: x.value,
flag: x.flag, flag: x.flag,
comment: x.user comment: comment ? x.user + ' • ' + comment : x.user
}; };
return vitals_mapping[x.name] ? Object.assign(res, vitals_mapping[x.name]) : res; return vitals_mapping[x.name] ? Object.assign(res, vitals_mapping[x.name]) : res;
}); });

View File

@ -53,15 +53,15 @@ function lab_parse1default(data) {
else x.comment = [line.substring(12)]; else x.comment = [line.substring(12)];
} else console.log('DANGLING:', line); } else console.log('DANGLING:', line);
} else if(m = line.match(/^\b(?<name>.*?)\s{2,}(?<value>.*?)(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/)) { } else if(m = line.match(/^\b(?<name>.*?)\s{2,}(?<value>.*?)(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/)) {
if(x = line.match(/^\b(?<name>.*?)(?<value>(?:positive|negative|reactive|nonreactive|not detected|collected - specimen in lab|test not performed))(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/i)) m = x; if(x = line.match(/^\b(?<name>.*?)(?<value>(?:positive|negative|reactive|nonreactive|detected|not detected|comment|collected - specimen in lab|test not performed))(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/i)) m = x;
if((m.groups.range) && (m.groups.range.startsWith('Ref: '))) m.groups.range = m.groups.range.substring(5); if((m.groups.range) && (m.groups.range.startsWith('Ref: '))) m.groups.range = m.groups.range.substring(5);
results.push(x = m.groups); results.push(x = m.groups);
if((x.value === '') && (m = x.name.match(/^(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|not detected|collected - specimen in lab|test not performed))\s*$/i))) { if((x.value === '') && (m = x.name.match(/^(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|detected|not detected|comment|collected - specimen in lab|test not performed))\s*$/i))) {
x.name = m.groups.name; x.name = m.groups.name;
x.value = m.groups.value; x.value = m.groups.value;
} }
for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined; for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined;
} else if(m = line.match(/^\b(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|nonreactive|not detected|collected - specimen in lab|test not performed))\s*$/i)) { } else if(m = line.match(/^\b(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|nonreactive|detected|not detected|comment|collected - specimen in lab|test not performed))(?: (?<flag>L\*|L|H\*|H))?\s*$/i)) {
results.push(x = m.groups); results.push(x = m.groups);
for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined; for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined;
} else if(line.startsWith(' [')) { } else if(line.startsWith(' [')) {
@ -95,7 +95,7 @@ function lab_parse1default(data) {
value: x = (results.hasOwnProperty('SEGS') ? +results.SEGS.value : 0) + (results.hasOwnProperty('BANDS') ? +results.BANDS.value : 0), value: x = (results.hasOwnProperty('SEGS') ? +results.SEGS.value : 0) + (results.hasOwnProperty('BANDS') ? +results.BANDS.value : 0),
flag: (x < 42.2 ? 'L' : x > 75.2 ? 'H' : undefined) flag: (x < 42.2 ? 'L' : x > 75.2 ? 'H' : undefined)
}); });
results.push(results['NEUTROPHIL#'] = { if(results.WBC) results.push(results['NEUTROPHIL#'] = {
name: 'NEUTROPHIL#', unit: 'K/cmm', range: '1.4 - 6.5', name: 'NEUTROPHIL#', unit: 'K/cmm', range: '1.4 - 6.5',
value: +(x = 0.01*x*results.WBC.value).toFixed(3), value: +(x = 0.01*x*results.WBC.value).toFixed(3),
flag: (x < 1.4 ? 'L' : x > 6.5 ? 'H' : undefined) flag: (x < 1.4 ? 'L' : x > 6.5 ? 'H' : undefined)
@ -107,7 +107,7 @@ function lab_parse1default(data) {
value: x = +results.EOSINO.value, value: x = +results.EOSINO.value,
flag: (x < 0 ? 'L' : x > 10 ? 'H' : undefined) flag: (x < 0 ? 'L' : x > 10 ? 'H' : undefined)
}); });
results.push(results['EOSINOPHIL#'] = { if(results.WBC) results.push(results['EOSINOPHIL#'] = {
name: 'EOSINOPHIL#', unit: 'K/cmm', range: '0.0 - 0.7', name: 'EOSINOPHIL#', unit: 'K/cmm', range: '0.0 - 0.7',
value: +(x = 0.01*x*results.WBC.value).toFixed(3), value: +(x = 0.01*x*results.WBC.value).toFixed(3),
flag: (x < 0 ? 'L' : x > 0.7 ? 'H' : undefined) flag: (x < 0 ? 'L' : x > 0.7 ? 'H' : undefined)
@ -119,7 +119,7 @@ function lab_parse1default(data) {
value: x = +results.BASO.value, value: x = +results.BASO.value,
flag: (x < 0 ? 'L' : x > 2 ? 'H' : undefined) flag: (x < 0 ? 'L' : x > 2 ? 'H' : undefined)
}); });
results.push(results['BASOPHIL#'] = { if(results.WBC) results.push(results['BASOPHIL#'] = {
name: 'BASOPHIL#', unit: 'K/cmm', range: '0.0 - 0.2', name: 'BASOPHIL#', unit: 'K/cmm', range: '0.0 - 0.2',
value: +(x = 0.01*x*results.WBC.value).toFixed(3), value: +(x = 0.01*x*results.WBC.value).toFixed(3),
flag: (x < 0 ? 'L' : x > 0.2 ? 'H' : undefined) flag: (x < 0 ? 'L' : x > 0.2 ? 'H' : undefined)
@ -131,7 +131,7 @@ function lab_parse1default(data) {
value: x = +results.MONOS.value, value: x = +results.MONOS.value,
flag: (x < 1.7 ? 'L' : x > 9.3 ? 'H' : undefined) flag: (x < 1.7 ? 'L' : x > 9.3 ? 'H' : undefined)
}); });
results.push(results['MONOCYTE#'] = { if(results.WBC) results.push(results['MONOCYTE#'] = {
name: 'MONOCYTE#', unit: 'K/cmm', range: '0.11 - 0.59', name: 'MONOCYTE#', unit: 'K/cmm', range: '0.11 - 0.59',
value: +(x = 0.01*x*results.WBC.value).toFixed(3), value: +(x = 0.01*x*results.WBC.value).toFixed(3),
flag: (x < 0.11 ? 'L' : x > 0.59 ? 'H' : undefined) flag: (x < 0.11 ? 'L' : x > 0.59 ? 'H' : undefined)
@ -143,7 +143,7 @@ function lab_parse1default(data) {
value: x = (results.hasOwnProperty('LYMPHS') ? +results.LYMPHS.value : 0) + (results.hasOwnProperty('ATYPICAL LYMPHOCYTES') ? +results['ATYPICAL LYMPHOCYTES'].value : 0), value: x = (results.hasOwnProperty('LYMPHS') ? +results.LYMPHS.value : 0) + (results.hasOwnProperty('ATYPICAL LYMPHOCYTES') ? +results['ATYPICAL LYMPHOCYTES'].value : 0),
flag: (x < 15 ? 'L' : x > 41 ? 'H' : undefined) flag: (x < 15 ? 'L' : x > 41 ? 'H' : undefined)
}); });
results.push(results['LYMPHOCYTE#'] = { if(results.WBC) results.push(results['LYMPHOCYTE#'] = {
name: 'LYMPHOCYTE#', unit: 'K/cmm', range: '1.2 - 3.4', name: 'LYMPHOCYTE#', unit: 'K/cmm', range: '1.2 - 3.4',
value: +(x = 0.01*x*results.WBC.value).toFixed(3), value: +(x = 0.01*x*results.WBC.value).toFixed(3),
flag: (x < 1.2 ? 'L' : x > 3.4 ? 'H' : undefined) flag: (x < 1.2 ? 'L' : x > 3.4 ? 'H' : undefined)
@ -195,20 +195,36 @@ export function measurement_parse(data) {
res.name = row.substring(idx + 3, idx = row.indexOf(': ', idx)); res.name = row.substring(idx + 3, idx = row.indexOf(': ', idx));
value = row.substring(idx + 4, idx = row.indexOf(' _', idx)); value = row.substring(idx + 4, idx = row.indexOf(' _', idx));
res.user = row.substring(idx + 2); res.user = row.substring(idx + 2);
m = value.match(/^(?:(.*?)(?: (\S+))?)(\*)?(?: \((?:(.*?)(?: (\S+))?)\))?\s*$/); if(m = value.match(/(?:^(?<value>[\d\.\/%]+)(?: (?<unit>\w+) \((?<value2>[\d\.\/%]+) (?<unit2>\w+)\))?(?<flag>\*)? (?: (?<comment>.*))?$)|(?:^(?<value3>[\d\.\/%]+)(?<flag3>\*)?\s*(?<comment3>.*)$)/)) {
res.value = m[4] ? m[4] : m[1]; if(m.groups.value2) {
res.unit = m[4] ? m[5] : m[2]; res.value = m.groups.value2;
res.flag = m[3]; res.unit = m.groups.unit2;
res.value_american = m[4] ? m[1] : m[4]; res.value_american = m.groups.value;
res.unit_american = m[4] ? m[2] : m[5]; res.unit_american = m.groups.unit;
if(res.value.charAt(res.value.length - 1) == '%') { res.flag = m.groups.flag;
res.unit = '%'; res.comment = m.groups.comment;
res.value = res.value.substring(0, res.value.length - 1); } else if(m.groups.value) {
res.value = m.groups.value;
res.unit = m.groups.unit;
res.flag = m.groups.flag;
res.comment = m.groups.comment;
} else if(m.groups.value3) {
res.value = m.groups.value3;
res.flag = m.groups.flag3;
res.comment = m.groups.comment3;
} else res.comment = value;
if(res.comment) res.comment = res.comment.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
} }
if(res.name == 'B/P') { if(res.value) {
var bpsplit = res.value.split('/'); if(res.value.charAt(res.value.length - 1) == '%') {
extras.push({...res, name: 'SBP', range: '90 - 120', unit: 'mmHg', value: bpsplit[0] }); res.unit = '%';
extras.push({...res, name: 'DBP', range: '60 - 80', unit: 'mmHg', value: bpsplit[1] }); res.value = res.value.substring(0, res.value.length - 1);
}
if(res.name == 'B/P') {
var bpsplit = res.value.split('/');
extras.push({...res, name: 'SBP', range: '90 - 120', unit: 'mmHg', value: bpsplit[0] });
extras.push({...res, name: 'DBP', range: '60 - 80', unit: 'mmHg', value: bpsplit[1] });
}
} }
return res; return res;
} }

View File

@ -112,8 +112,8 @@ def application():
client._cache_persistent(persistent=util.Store(f'cache.{client._server["volume"].lower()}.{client._server["uci"].lower()}.{user[0]}.db', journal_mode='WAL').memo) client._cache_persistent(persistent=util.Store(f'cache.{client._server["volume"].lower()}.{client._server["uci"].lower()}.{user[0]}.db', journal_mode='WAL').memo)
return jsonify_result(user, id=request.json.get('id')) return jsonify_result(user, id=request.json.get('id'))
else: else:
from auth import XUIAMSSOi_MySsoTokenVBA import XWBSSOi
if token := XUIAMSSOi_MySsoTokenVBA(): if token := XWBSSOi.get_sso_token(application='CPRSChart.exe'):
user = client.authenticate(token) user = client.authenticate(token)
client._cache_persistent(persistent=util.Store(f'cache.{client._server["volume"].lower()}.{client._server["uci"].lower()}.{user[0]}.db', journal_mode='WAL').memo) client._cache_persistent(persistent=util.Store(f'cache.{client._server["volume"].lower()}.{client._server["uci"].lower()}.{user[0]}.db', journal_mode='WAL').memo)
return jsonify_result(user, id=request.json.get('id')) return jsonify_result(user, id=request.json.get('id'))

4
rpc.py
View File

@ -237,13 +237,13 @@ class ClientAsync(object):
if __name__ == '__main__': if __name__ == '__main__':
import getpass, code import getpass, code
from auth import XUIAMSSOi_MySsoTokenVBA import XWBSSOi
client = ClientSync(host='test.northport.med.va.gov', port=19009) client = ClientSync(host='test.northport.med.va.gov', port=19009)
#client = ClientSync(host='vista.northport.med.va.gov', port=19209) #client = ClientSync(host='vista.northport.med.va.gov', port=19209)
threading.Thread(target=client.keepalive, daemon=True).start() threading.Thread(target=client.keepalive, daemon=True).start()
print('\r\n'.join(client.XUS_INTRO_MSG())) print('\r\n'.join(client.XUS_INTRO_MSG()))
if token := XUIAMSSOi_MySsoTokenVBA(): if token := XWBSSOi.get_sso_token(application='CPRSChart.exe'):
print('authenticate', repr(client.authenticate(token))) print('authenticate', repr(client.authenticate(token)))
else: else:
print('authenticate', repr(client.authenticate(f"{getpass.getpass('ACCESS CODE: ')};{getpass.getpass('VERIFY CODE: ')}"))) print('authenticate', repr(client.authenticate(f"{getpass.getpass('ACCESS CODE: ')};{getpass.getpass('VERIFY CODE: ')}")))