Compare commits

..

3 Commits

Author SHA1 Message Date
1a08acdc7c Host switching 2022-09-26 17:38:27 -04:00
31ffadbae3 Store and verify connection parameters 2022-09-26 11:15:14 -04:00
5035ff2dd1 Fix end date reporting and partial year rejection 2022-09-26 10:20:32 -04:00
8 changed files with 162 additions and 46 deletions

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="container-fluid"> <div class="container-fluid">
<Navbar :user="user" /> <Navbar v-model:server="server" :user="user" />
<div class="container"> <div class="container">
<Login :secret="secret" v-model:client="client" v-model:server="server" v-model:user="user" />
<router-view v-if="user"></router-view> <router-view v-if="user"></router-view>
<Login :secret="secret" v-model:client="client" v-model:user="user" />
</div> </div>
</div> </div>
</template> </template>
@ -26,6 +26,7 @@
data() { data() {
return { return {
client: null, client: null,
server: null,
user: null, user: null,
heartbeat: null, heartbeat: null,
banner: '', banner: '',
@ -36,7 +37,7 @@
store: () => store store: () => store
}, },
watch: { watch: {
async client(value) { async client(value, oldvalue) {
if(this.heartbeat) window.clearInterval(this.heartbeat); if(this.heartbeat) window.clearInterval(this.heartbeat);
else { else {
[ [
@ -47,7 +48,8 @@
].forEach(route => this.$root.$router.addRoute(route)); ].forEach(route => this.$root.$router.addRoute(route));
await this.$root.$router.replace(this.$route); await this.$root.$router.replace(this.$route);
} }
this.heartbeat = await value.heartbeat(); if(value) this.heartbeat = await value.heartbeat();
if(oldvalue) this.$router.go(); // refresh if changed
} }
} }
}; };

View File

@ -104,7 +104,8 @@
data() { data() {
return { return {
x_range: this.range, x_range: this.range,
x_date: this.date x_date: this.date,
x_date_end: this.date_end
}; };
}, },
computed: { computed: {
@ -113,8 +114,10 @@
return this.x_date.toLocaleDateString('en-CA'); return this.x_date.toLocaleDateString('en-CA');
}, },
set(value) { set(value) {
value = value.split('-') if(value.length > 0) {
this.x_date = new Date(value[0], value[1] - 1, value[2]); value = value.split('-');
if(value[0] >= 1700) this.x_date = new Date(value[0], value[1] - 1, value[2]);
}
} }
}, },
disp_date_end: { disp_date_end: {
@ -122,8 +125,10 @@
return this.x_date_end.toLocaleDateString('en-CA'); return this.x_date_end.toLocaleDateString('en-CA');
}, },
set(value) { set(value) {
value = value.split('-') if(value.length > 0) {
this.x_date_end = new Date(value[0], value[1] - 1, value[2]); value = value.split('-');
if(value[0] >= 1700) this.x_date_end = new Date(value[0], value[1] - 1, value[2]);
}
} }
}, },
params() { params() {
@ -142,6 +147,8 @@
}, },
date(value) { this.x_date = value; }, date(value) { this.x_date = value; },
x_date(value) { this.$emit('update:date', value); }, x_date(value) { this.$emit('update:date', value); },
date_end(value) { this.x_date_end = value; },
x_date_end(value) { this.$emit('update:date_end', value); },
range(value) { this.x_range = value; }, range(value) { this.x_range = value; },
x_range(value) { this.$emit('update:range', value); } x_range(value) { this.$emit('update:range', value); }
}, },

View File

@ -1,25 +1,60 @@
<template> <template>
<div class="card mb-3 shadow"> <div class="accordion mb-3 shadow">
<div class="card-header"><template v-if="user">{{user[2]}}</template><template v-else>Login</template></div> <div class="accordion-item">
<div class="card-body"> <h2 class="accordion-header"><button class="accordion-button" :class="{ testing: (x_server) && (x_server.production != '1') }" type="button" @click="() => show = !show"><template v-if="user">{{user[2]}}<template v-if="server"> @ {{server.domain}}</template></template><template v-else>Login</template></button></h2>
<p class="card-text row"><code class="col" v-if="banner"><pre>{{banner.join('\n')}}</pre></code><code class="col" v-if="user"><pre>{{user.join('\n')}}</pre></code></p> <div class="accordion-collapse collapse" :class="{ show }">
</div> <div class="accordion-body">
<div class="input-group flex-nowrap" v-if="!user"> <div class="card">
<span class="input-group-text">🔑</span> <div class="card-body">
<input type="password" class="form-control" placeholder="Access Code" v-model="accesscode" /> <p class="card-text row"><code class="col" v-if="banner"><pre>{{banner.join('\n')}}</pre></code><code class="col" v-if="user"><pre>{{user.join('\n')}}</pre></code></p>
<input type="password" class="form-control" placeholder="Verify Code" v-model="verifycode" /> </div>
<button class="btn btn-primary" type="button" v-on:click="submit">Login<template v-if="!(accesscode || verifycode)"> (omit AV codes for SAML)</template></button> <button class="btn btn-danger" style="width: 100%;" type="button" v-if="user" v-on:click="logout">Logout</button>
<div class="input-group flex-nowrap" v-if="!user">
<span class="input-group-text">🔑</span>
<select class="form-control" v-model="host">
<option value="vista.bronx.med.va.gov:19201">Bronx BRX</option>
<option value="test.bronx.med.va.gov:19001">Bronx BRX-TEST</option>
<option value="vista.east-orange.med.va.gov:19203">East Orange NJH</option>
<option value="test.east-orange.med.va.gov:19003">East Orange NJH-TEST</option>
<option value="vista.hudson-valley.med.va.gov:19205">Hudson Valley NVH</option>
<option value="test.hudson-valley.med.va.gov:19005">Hudson Valley NVH-TEST</option>
<option value="vista.brooklyn.med.va.gov:19208">NY Harbor NYH</option>
<option value="test.brooklyn.med.va.gov:19008">NY Harbor NYH-TEST</option>
<option value="vista.northport.med.va.gov:19209">Northport NOP</option>
<option value="test.northport.med.va.gov:19009">Northport NOP-TEST</option>
<option value="vista.v02.med.va.gov:19224">VISN 2 V02</option>
<option value="test.v02.med.va.gov:19024">VISN 2 V02-TEST</option>
</select>
<input type="password" class="form-control" placeholder="Access Code" v-model="accesscode" />
<input type="password" class="form-control" placeholder="Verify Code" v-model="verifycode" />
<button class="btn btn-primary" type="button" v-on:click="login">Login<template v-if="!(accesscode || verifycode)"> (omit AV codes for SAML)</template></button>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.accordion-button.testing:not(.collapsed) {
color: #e40c63;
background-color: #ffe7f1;
}
</style>
<script> <script>
import cookie from './cookie.mjs';
import vistax from './vistax.mjs'; import vistax from './vistax.mjs';
export default { export default {
props: { props: {
secret: String, secret: String,
client: Object, client: Object,
server: {
type: Object,
default: null
},
user: { user: {
type: Array, type: Array,
default: null default: null
@ -27,6 +62,10 @@
}, },
emits: { emits: {
'update:client': Object, 'update:client': Object,
'update:server': {
type: Object,
default: null
},
'update:user': { 'update:user': {
type: Array, type: Array,
default: null default: null
@ -34,7 +73,10 @@
}, },
data() { data() {
return { return {
show: false,
host: cookie.get('host'),
x_client: this.client, x_client: this.client,
x_server: this.server,
x_user: this.user, x_user: this.user,
banner: null, banner: null,
accesscode: null, accesscode: null,
@ -42,33 +84,57 @@
}; };
}, },
watch: { watch: {
host(value) {
cookie.set('host', value);
this.logout();
},
client(value) { this.x_client = value; }, client(value) { this.x_client = value; },
x_client(value) { this.$emit('update:client', value); }, x_client(value) { this.$emit('update:client', value); },
server(value) { this.x_server = value; },
x_server(value) { this.$emit('update:server', value); },
user(value) { this.x_user = value; }, user(value) { this.x_user = value; },
x_user(value) { this.$emit('update:user', value); } x_user(value) { this.$emit('update:user', value); }
}, },
async mounted() { async mounted() {
this.x_client = await vistax.Client.fromCookie(this.secret); this.connect();
this.banner = await this.x_client.XUS_INTRO_MSG();
if((await this.x_client.userinfo()).result) try {
var user = await this.x_client.XUS_GET_USER_INFO();
this.x_user = user[0] ? user : null
} catch(ex) {
this.x_user = null;
}
this.$emit('update:user', this.x_user);
console.log('Backend secret', this.secret);
console.log(this.banner);
}, },
methods: { methods: {
async submit(evt) { async connect() {
if(this.x_client) return this.x_client;
this.x_client = await vistax.Client.fromCookie(this.secret);
this.banner = await this.x_client.XUS_INTRO_MSG();
if((await this.x_client.userinfo()).result) try {
var user = await this.x_client.XUS_GET_USER_INFO();
this.x_user = user[0] ? user : null
} catch(ex) {
this.x_user = null;
}
this.$emit('update:user', this.x_user);
this.show = !this.x_user;
this.$emit('update:server', this.x_server = (await this.x_client.serverinfo()).result);
console.log('Backend secret', this.secret);
console.log(this.banner);
return this.x_client;
},
async login(evt) {
if(!this.x_client) await this.connect();
var res = await ((this.accesscode && this.verifycode) ? this.x_client.authenticate(this.accesscode + ';' + this.verifycode) : this.x_client.authenticate()); var res = await ((this.accesscode && this.verifycode) ? this.x_client.authenticate(this.accesscode + ';' + this.verifycode) : this.x_client.authenticate());
if(!!res.result[0]) { if(!!res.result[0]) {
var user = await this.x_client.XUS_GET_USER_INFO(); var user = await this.x_client.XUS_GET_USER_INFO();
this.x_user = user[0] ? user : null this.x_user = user[0] ? user : null
} else this.x_user = null; } else this.x_user = null;
this.$emit('update:user', this.x_user); this.$emit('update:user', this.x_user);
this.show = !this.x_user;
this.$emit('update:server', this.x_server = (await this.x_client.serverinfo()).result);
console.log('Authenticate', res); console.log('Authenticate', res);
},
async logout(evt) {
if(this.x_client) {
console.log('Close', await this.x_client.close());
this.$emit('update:client', this.x_client = null);
this.$emit('update:server', this.x_server = null);
this.$emit('update:user', this.x_user = null);
}
} }
} }
}; };

View File

@ -16,8 +16,8 @@
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" to="/recall">Recall</router-link> <router-link class="nav-link" to="/recall">Recall</router-link>
</li> </li>
<li class="nav-item" v-if="user"> <li class="nav-item" v-if="server">
<a class="nav-link disabled">{{user[3]}}</a> <a class="nav-link disabled">{{server.domain}}</a>
</li> </li>
</ul> </ul>
<form class="d-flex" role="search"> <form class="d-flex" role="search">
@ -34,6 +34,10 @@
export default { export default {
props: { props: {
server: {
type: Object,
default: null
},
user: { user: {
type: Array, type: Array,
default: null default: null

View File

@ -6,6 +6,14 @@ export async function connect(secret, host='vista.northport.med.va.gov', port=19
})).json(); })).json();
} }
export async function close(cid) {
return await (await fetch('/v1/vista/' + cid + '/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: Date.now() })
})).json();
}
export async function call(cid, method, ...params) { export async function call(cid, method, ...params) {
return await (await fetch('/v1/vista/' + cid, { return await (await fetch('/v1/vista/' + cid, {
method: 'POST', method: 'POST',
@ -47,5 +55,5 @@ export async function authenticate(cid, avcode=null) {
} }
export default window.vista = { export default window.vista = {
connect, call, callctx, serverinfo, userinfo, authenticate connect, close, call, callctx, serverinfo, userinfo, authenticate
}; };

View File

@ -2,6 +2,8 @@ import vista from './vista.mjs';
import cookie from './cookie.mjs'; import cookie from './cookie.mjs';
import { lab_parse, lab_reparse_results, measurement_parse } from './reportparser.mjs'; import { lab_parse, lab_reparse_results, measurement_parse } from './reportparser.mjs';
const COOKIE_TIME = 45;
function RPCError(type, ...args) { function RPCError(type, ...args) {
this.name = type; this.name = type;
this.message = args; this.message = args;
@ -75,6 +77,10 @@ export function Client(cid, secret) {
this.secret = secret; this.secret = secret;
this.cid = cid; this.cid = cid;
this.close = function() {
if(heartbeat) window.clearInterval(heartbeat);
return vista.close(cid);
};
this.call = (method, ...params) => vista.call(cid, method, ...params); this.call = (method, ...params) => vista.call(cid, method, ...params);
this.callctx = (context, method, ...params) => vista.callctx(cid, context, method, ...params); this.callctx = (context, method, ...params) => vista.callctx(cid, context, method, ...params);
this.heartbeat = async function(interval=null) { this.heartbeat = async function(interval=null) {
@ -124,13 +130,16 @@ Client.fromScratch = async function(secret, host='vista.northport.med.va.gov', p
if(data.result) return Client.fromID(data.result, secret); if(data.result) return Client.fromID(data.result, secret);
}; };
Client.fromCookie = async function(secret, host='vista.northport.med.va.gov', port=19209) { Client.fromCookie = async function(secret, defaulthost='vista.northport.med.va.gov:19209') {
if(!secret) secret = cookie.get('secret'); if(!secret) secret = cookie.get('secret');
if(secret) { if(secret) {
var host = cookie.get('host');
host = (host || defaulthost).split(':');
if(secret != cookie.get('secret')) { if(secret != cookie.get('secret')) {
console.log('Using new secret', secret); console.log('Using new secret', secret);
var client = await Client.fromScratch(secret, host, port); var client = await Client.fromScratch(secret, host[0], host[1]);
if(client) { if(client) {
cookie.set('host', host.join(':'), COOKIE_TIME);
cookie.set('secret', secret); cookie.set('secret', secret);
cookie.set('cid', client.cid); cookie.set('cid', client.cid);
console.log('Established connection', client.cid); console.log('Established connection', client.cid);
@ -143,8 +152,9 @@ Client.fromCookie = async function(secret, host='vista.northport.med.va.gov', po
} }
} else if(!cookie.get('cid')) { } else if(!cookie.get('cid')) {
console.log('Using saved secret', secret); console.log('Using saved secret', secret);
var client = await Client.fromScratch(secret, host, port); var client = await Client.fromScratch(secret, host[0], host[1]);
if(client) { if(client) {
cookie.set('host', host.join(':'), COOKIE_TIME);
cookie.set('secret', secret); cookie.set('secret', secret);
cookie.set('cid', client.cid); cookie.set('cid', client.cid);
console.log('Established connection', client.cid); console.log('Established connection', client.cid);
@ -159,9 +169,15 @@ Client.fromCookie = async function(secret, host='vista.northport.med.va.gov', po
console.log('Using saved secret and connection', secret); console.log('Using saved secret and connection', secret);
var cid = cookie.get('cid'); var cid = cookie.get('cid');
var client = Client.fromID(cid, secret); var client = Client.fromID(cid, secret);
if((await vista.call(cid, 'XWB_IM_HERE')).result == '1') return client; if((await vista.call(cid, 'XWB_IM_HERE')).result == '1') {
var server = await client.serverinfo();
if((host[0] == server.result.host) && (host[1] == server.result.port)) {
cookie.set('host', host.join(':'), COOKIE_TIME);
return client;
} else console.log('Rejecting previous connection to different server', server);
}
cookie.reset('cid'); cookie.reset('cid');
return await Client.fromCookie(secret, host, port); return await Client.fromCookie(secret, host.join(':'));
} }
} }
}; };

17
main.py
View File

@ -62,11 +62,22 @@ def application():
else: else:
return jsonify({ 'result': None, 'error': { 'type': 'Unauthorized', 'args': [] }, 'id': request.json.get('id') }) return jsonify({ 'result': None, 'error': { 'type': 'Unauthorized', 'args': [] }, 'id': request.json.get('id') })
@app.post('/v1/vista/<cid>/close')
def cb_close(cid):
try:
client = clients[cid]
res = client.close()
del clients[cid]
return jsonify({ 'result': res, 'error': None, 'id': request.json.get('id') })
except Exception as ex:
logger.exception(request.url)
return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') })
@app.post('/v1/vista/<cid>/serverinfo') @app.post('/v1/vista/<cid>/serverinfo')
def cb_serverinfo(cid): def cb_serverinfo(cid):
try: try:
client = clients[cid] client = clients[cid]
return jsonify({ 'result': client._obj._server._asdict() if client._obj._server else None, 'error': None, 'id': request.json.get('id') }) return jsonify({ 'result': client._obj._server, 'error': None, 'id': request.json.get('id') })
except Exception as ex: except Exception as ex:
logger.exception(request.url) logger.exception(request.url)
return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') })
@ -87,13 +98,13 @@ def application():
client = clients[cid] client = clients[cid]
if 'avcode' in params: if 'avcode' in params:
user = client.authenticate(params['avcode']) user = client.authenticate(params['avcode'])
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, 'error': None, 'id': request.json.get('id') }) return jsonify({ 'result': user, 'error': None, 'id': request.json.get('id') })
else: else:
from auth import XUIAMSSOi_MySsoTokenVBA from auth import XUIAMSSOi_MySsoTokenVBA
if token := XUIAMSSOi_MySsoTokenVBA(): if token := XUIAMSSOi_MySsoTokenVBA():
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, 'error': None, 'id': request.json.get('id') }) return jsonify({ 'result': user, 'error': None, 'id': request.json.get('id') })
else: else:
return jsonify({ 'result': None, 'error': { 'type': 'Unauthorized', 'args': [] }, 'id': request.json.get('id') }) return jsonify({ 'result': None, 'error': { 'type': 'Unauthorized', 'args': [] }, 'id': request.json.get('id') })

10
rpc.py
View File

@ -99,7 +99,8 @@ class ClientSync(object):
self.sock.connect((host, port)) self.sock.connect((host, port))
self.recv_rpc_msg = recv_rpc_msg(self.sock) self.recv_rpc_msg = recv_rpc_msg(self.sock)
self.lock = threading.Lock() self.lock = threading.Lock()
self._server = self._user = None self._server = { 'host': host, 'port': port }
self._user = None
self.context = 'XUS SIGNON' self.context = 'XUS SIGNON'
if TCPConnect and (res := self.TCPConnect(self.sock.getsockname()[0], '0', socket.gethostname())) != 'accept': if TCPConnect and (res := self.TCPConnect(self.sock.getsockname()[0], '0', socket.gethostname())) != 'accept':
raise RPCExcInvalidResult('TCPConnect', self.sock.getsockname()[0], '0', socket.gethostname(), res) raise RPCExcInvalidResult('TCPConnect', self.sock.getsockname()[0], '0', socket.gethostname(), res)
@ -129,7 +130,7 @@ class ClientSync(object):
self.sock = self.recv_rpc_msg = None self.sock = self.recv_rpc_msg = None
return res return res
def authenticate(self, identity: str, *, context=('XUS SIGNON',)): def authenticate(self, identity: str, *, context=('XUS SIGNON',)):
self._server = RecordServerInfo(*self('XUS SIGNON SETUP', '', '1', context=context)) self._server.update(RecordServerInfo(*self('XUS SIGNON SETUP', '', '1', context=context))._asdict())
res = self('XUS AV CODE', XWBHash_encrypt(identity)) res = self('XUS AV CODE', XWBHash_encrypt(identity))
if res[0] == '0' or res[2] != '0': if res[0] == '0' or res[2] != '0':
raise RPCExcAuth(res[3], res) raise RPCExcAuth(res[3], res)
@ -170,7 +171,8 @@ class ClientAsync(object):
self.reader, self.writer = await asyncio.open_connection(host, port) self.reader, self.writer = await asyncio.open_connection(host, port)
self.arecv_rpc_msg = arecv_rpc_msg(self.reader) self.arecv_rpc_msg = arecv_rpc_msg(self.reader)
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self._server = self._user = None self._server = { 'host': host, 'port': port, 'info': None }
self._user = None
self.context = 'XUS SIGNON' self.context = 'XUS SIGNON'
if TCPConnect and (res := await self.TCPConnect(self.writer.get_extra_info('sockname')[0], '0', socket.gethostname())) != 'accept': if TCPConnect and (res := await self.TCPConnect(self.writer.get_extra_info('sockname')[0], '0', socket.gethostname())) != 'accept':
raise RPCExcInvalidResult('TCPConnect', self.writer.get_extra_info('sockname')[0], '0', socket.gethostname(), res) raise RPCExcInvalidResult('TCPConnect', self.writer.get_extra_info('sockname')[0], '0', socket.gethostname(), res)
@ -205,7 +207,7 @@ class ClientAsync(object):
self.reader = self.writer = None self.reader = self.writer = None
return res return res
async def authenticate(self, identity: str, *, context=('XUS SIGNON',)): async def authenticate(self, identity: str, *, context=('XUS SIGNON',)):
self._server = RecordServerInfo(*await self('XUS SIGNON SETUP', '', '1', context=context)) self._server.update(RecordServerInfo(*await self('XUS SIGNON SETUP', '', '1', context=context))._asdict())
res = await self('XUS AV CODE', XWBHash_encrypt(identity)) res = await self('XUS AV CODE', XWBHash_encrypt(identity))
if res[0] == '0' or res[2] != '0': if res[0] == '0' or res[2] != '0':
raise RPCExcAuth(res[3], res) raise RPCExcAuth(res[3], res)