Merge pull request #3 from speatzle/client-auth

Add Client Auth, Session management and API call
This commit is contained in:
Samuel Lorch 2023-03-12 16:53:59 +01:00 committed by GitHub
commit b69cc43561
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 414 additions and 186 deletions

View file

@ -12,17 +12,19 @@
},
"dependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.2",
"@open-rpc/client-js": "^1.8.1",
"@vueuse/core": "^9.13.0",
"@vueuse/head": "^1.1.15",
"axios": "^1.3.4",
"events": "^3.3.0",
"focus-trap": "^7.3.1",
"focus-trap-vue": "^4.0.2",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-shiki": "^0.8.0",
"simple-jsonrpc-js": "^1.2.0",
"vue": "^3.2.45",
"vue-i18n": "9",
"vue-router": "4"
"vue-router": "4",
"ws": "^8.13.0"
},
"devDependencies": {
"@iconify/json": "^2.2.30",

146
client/pnpm-lock.yaml generated
View file

@ -3,7 +3,6 @@ lockfileVersion: 5.4
specifiers:
'@iconify/json': ^2.2.30
'@intlify/unplugin-vue-i18n': ^0.8.2
'@open-rpc/client-js': ^1.8.1
'@types/events': ^3.0.0
'@types/markdown-it-link-attributes': ^3.0.1
'@typescript-eslint/parser': ^5.54.1
@ -12,6 +11,7 @@ specifiers:
'@vue-macros/volar': ^0.8.4
'@vueuse/core': ^9.13.0
'@vueuse/head': ^1.1.15
axios: ^1.3.4
eslint: ^8.35.0
eslint-plugin-vue: ^9.9.0
events: ^3.3.0
@ -19,6 +19,7 @@ specifiers:
focus-trap-vue: ^4.0.2
markdown-it-link-attributes: ^4.0.1
markdown-it-shiki: ^0.8.0
simple-jsonrpc-js: ^1.2.0
typescript: ^4.9.3
unplugin-auto-import: ^0.15.0
unplugin-icons: ^0.15.3
@ -31,20 +32,23 @@ specifiers:
vue-i18n: '9'
vue-router: '4'
vue-tsc: ^1.0.24
ws: ^8.13.0
dependencies:
'@intlify/unplugin-vue-i18n': 0.8.2_vue-i18n@9.2.2
'@open-rpc/client-js': 1.8.1
'@vueuse/core': 9.13.0_vue@3.2.47
'@vueuse/head': 1.1.15_vue@3.2.47
axios: 1.3.4
events: 3.3.0
focus-trap: 7.3.1
focus-trap-vue: 4.0.2_oggptlzwchqpaguemspe4ract4
markdown-it-link-attributes: 4.0.1
markdown-it-shiki: 0.8.0
simple-jsonrpc-js: 1.2.0
vue: 3.2.47
vue-i18n: 9.2.2_vue@3.2.47
vue-router: 4.1.6_vue@3.2.47
ws: 8.13.0
devDependencies:
'@iconify/json': 2.2.30
@ -775,19 +779,6 @@ packages:
- supports-color
dev: true
/@open-rpc/client-js/1.8.1:
resolution: {integrity: sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==}
dependencies:
isomorphic-fetch: 3.0.0
isomorphic-ws: 5.0.0_ws@7.5.9
strict-event-emitter-types: 2.0.0
ws: 7.5.9
transitivePeerDependencies:
- bufferutil
- encoding
- utf-8-validate
dev: false
/@pkgr/utils/2.3.1:
resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@ -1507,11 +1498,25 @@ packages:
'@babel/types': 7.21.2
dev: true
/asynckit/0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
/available-typed-arrays/1.0.5:
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
engines: {node: '>= 0.4'}
dev: true
/axios/1.3.4:
resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==}
dependencies:
follow-redirects: 1.15.2
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
@ -1646,6 +1651,13 @@ packages:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
dev: true
/combined-stream/1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: false
/concat-map/0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
@ -1741,6 +1753,11 @@ packages:
resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==}
dev: true
/delayed-stream/1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: false
/destr/1.2.2:
resolution: {integrity: sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==}
dev: true
@ -2140,12 +2157,31 @@ packages:
tabbable: 6.1.1
dev: false
/follow-redirects/1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/for-each/0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
is-callable: 1.2.7
dev: true
/form-data/4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/fs-minipass/2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
@ -2572,23 +2608,6 @@ packages:
/isexe/2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
/isomorphic-fetch/3.0.0:
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
dependencies:
node-fetch: 2.6.9
whatwg-fetch: 3.6.2
transitivePeerDependencies:
- encoding
dev: false
/isomorphic-ws/5.0.0_ws@7.5.9:
resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
peerDependencies:
ws: '*'
dependencies:
ws: 7.5.9
dev: false
/jiti/1.17.1:
resolution: {integrity: sha512-NZIITw8uZQFuzQimqjUxIrIcEdxYDFIe/0xYfIlVXTkiBjjyBEvgasj5bb0/cHtPRD/NziPbT312sFrkI5ALpw==}
hasBin: true
@ -2800,6 +2819,18 @@ packages:
braces: 3.0.2
picomatch: 2.3.1
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types/2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mimic-fn/2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@ -2894,18 +2925,6 @@ packages:
resolution: {integrity: sha512-KIkvH1jl6b3O7es/0ShyCgWLcfXxlBrLBbP3rOr23WArC66IMcU4DeZEeYEOwnopYhawLTn7/y+YtmASe8DFVQ==}
dev: true
/node-fetch/2.6.9:
resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: false
/node-releases/2.0.10:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
dev: true
@ -3086,6 +3105,10 @@ packages:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: true
/proxy-from-env/1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
dev: false
/prr/1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
dev: true
@ -3235,6 +3258,10 @@ packages:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
/simple-jsonrpc-js/1.2.0:
resolution: {integrity: sha512-owkAmh7fjSYBUZVestTPCZMKYQvNiDejqZ/iGfVaKs1nrC1ZBDA3qGraf94+JNFJmu536Tb8oPe8PSPuq7GO6Q==}
dev: false
/sirv/2.0.2:
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
engines: {node: '>= 10'}
@ -3281,10 +3308,6 @@ packages:
internal-slot: 1.0.5
dev: true
/strict-event-emitter-types/2.0.0:
resolution: {integrity: sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==}
dev: false
/string_decoder/1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
@ -3393,10 +3416,6 @@ packages:
engines: {node: '>=6'}
dev: true
/tr46/0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
/tslib/1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true
@ -3846,10 +3865,6 @@ packages:
'@vue/server-renderer': 3.2.47_vue@3.2.47
'@vue/shared': 3.2.47
/webidl-conversions/3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/webpack-sources/3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'}
@ -3857,17 +3872,6 @@ packages:
/webpack-virtual-modules/0.5.0:
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
/whatwg-fetch/3.6.2:
resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==}
dev: false
/whatwg-url/5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: false
/which-boxed-primitive/1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies:
@ -3915,12 +3919,12 @@ packages:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
/ws/7.5.9:
resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==}
engines: {node: '>=8.3.0'}
/ws/8.13.0:
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true

View file

@ -3,22 +3,76 @@ import IDashboard from '~icons/ri/dashboard-2-line';
import IRule from '~icons/material-symbols/rule-folder-outline-sharp';
import IAddress from '~icons/eos-icons/ip';
import { authenticate, logout, checkAuthentication, setup } from "./api";
enum NavState { Open, Reduced, Collapsed };
const NavStateCount = 3;
let navState = $ref(NavState.Open);
let loggedOut = $ref(false);
const navRoutes = {
"/": { icon: IDashboard, caption: "Dashboard" },
"/rules": { icon: IRule, caption: "Rules" },
"/firewall/rules": { icon: IRule, caption: "Rules" },
"/addresses": { icon: IAddress, caption: "Addresses" },
};
enum AuthState { Unauthenticated, MfaRequired, Authenticated };
let authState = $ref(AuthState.Unauthenticated);
let loginDisabled = $ref(true);
let username = $ref("");
let password = $ref("");
async function tryLogin() {
loginDisabled = true;
const res = await authenticate(username, password);
password = "";
loginDisabled = false;
if (res.error != null) {
console.info("authentication error");
} else {
// TODO Check for MFA here
authState = AuthState.Authenticated;
}
}
async function tryLogout() {
console.info("Logging out...");
authState = AuthState.Unauthenticated;
logout();
}
function UnauthorizedCallback() {
console.info("Unauthenticated");
authState = AuthState.Unauthenticated;
}
async function checkAuth() {
console.info("Checking Auth State...");
let res = await checkAuthentication();
authState = res.auth;
loginDisabled = false;
if (authState === AuthState.Authenticated) {
console.info("Already Authenticated ", authState);
} else if (res.error == null) {
console.info("Unauthorized");
}
else console.info("Check Authentication error",res.error);
}
onMounted(async() => {
setup(UnauthorizedCallback);
await checkAuth();
setInterval(function () {
if (authState === AuthState.Authenticated && !document.hidden) {
checkAuth();
}
}.bind(this), 120000);
});
</script>
<template>
<div v-if="!loggedOut" :class="{
<div v-if="authState === AuthState.Authenticated" :class="{
'layout': 1,
'nav-state-open': navState === NavState.Open,
'nav-state-collapsed': navState === NavState.Collapsed,
@ -41,10 +95,10 @@ const navRoutes = {
<div class="flex-row">
<router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link>
<router-link class="button" to="/settings"><i-material-symbols-settings/></router-link>
<button @click="() => loggedOut = true"><i-material-symbols-logout/></button>
<button @click="tryLogout"><i-material-symbols-logout/></button>
</div>
</div>
<router-view v-slot="{ Component, route }" v-if="!loggedOut">
<router-view v-slot="{ Component, route }" v-if="authState === AuthState.Authenticated">
<Transition name="fade">
<component :is="Component" :key="{route}" class="page-content pad gap"/>
</Transition>
@ -52,16 +106,16 @@ const navRoutes = {
</div>
<Transition name="fade">
<div class="login" v-if="loggedOut">
<div class="login" v-if="authState === AuthState.Unauthenticated">
<FocusTrap>
<form @submit="$event => $event.preventDefault()">
<form @submit="$event => $event.preventDefault()" :disabled="loginDisabled">
<h1>nfSense Login</h1>
<label for="username" v-text="'Username'"/>
<input name="username"/>
<label for="password" v-text="'Password'" type="password"/>
<input name="password"/>
<button @click="() => loggedOut = false">Login</button>
<h2 :hidden="!loginDisabled">Logging in...</h2>
<label for="username" v-text="'Username'" :hidden="loginDisabled" />
<input name="username" v-model="username" :hidden="loginDisabled" :disabled="loginDisabled"/>
<label for="password" v-text="'Password'" type="password" :hidden="loginDisabled"/>
<input name="password" v-model="password" :hidden="loginDisabled" :disabled="loginDisabled"/>
<button @click="tryLogin">Login</button>
</form>
</FocusTrap>
</div>

View file

@ -1,14 +1,71 @@
import { RequestManager, HTTPTransport, WebSocketTransport, Client } from "@open-rpc/client-js";
const httpTransport = new HTTPTransport("http://"+ window.location.host +"/api");
const socktransport = new WebSocketTransport("ws://"+ window.location.host + "/ws/api");
const manager = new RequestManager([socktransport, httpTransport], () => crypto.randomUUID());
const client = new Client(manager);
// import WebSocketServer from 'ws';
import JsonRPC from 'simple-jsonrpc-js';
import axios from "axios";
export async function apiCall(method: string, params: Record<string, any>){
let jrpc = new JsonRPC.connect_xhr('/api');
// let socket = new WebSocket("ws://"+ window.location.host +"/ws/api");
let UnauthorizedCallback: Function;
export function setup(_UnauthorizedCallback: () => void) {
UnauthorizedCallback = _UnauthorizedCallback;
}
export async function apiCall(method: string, params: Record<string, any>): Promise<any>{
console.debug("Starting API Call...");
try {
const result = await client.request({method, params});
const result = await jrpc.call(method, params);
console.debug("api call result", result);
} catch (ex){
return { Data: result, Error: null};
} catch (ex: any){
if (ex.code === 401) {
UnauthorizedCallback();
} else {
console.debug("api call epic fail", ex);
}
return { Data: null, Error: ex};
}
}
export async function authenticate(username: string, password: string): Promise<any> {
const pResponse = axios.post("/login", { username, password }, {timeout: 10100});
try {
const response = await pResponse;
// Dont log this as the user password is inside: console.debug(response);
return { data: response.data, error: null};
} catch (error) {
return { data: null, error: error};
}
}
export async function logout(): Promise<any> {
const pResponse = axios.post("/logout", null, {timeout: 10100});
try {
const response = await pResponse;
return { data: response.data, error: null};
} catch (error) {
return { data: null, error: error};
}
}
export async function checkAuthentication() {
const pResponse = axios.post("/session", null, {timeout: 10100});
try {
const response = await pResponse;
const last_hash = window.localStorage.getItem("commit_hash");
if (last_hash) {
if (last_hash !== response.data.commit_hash) {
console.log("Detected New Backend Version, Reloading...");
window.localStorage.removeItem("commit_hash");
window.location.reload();
}
} else window.localStorage.setItem("commit_hash", response.data.commit_hash);
return {auth: 2, error: null};
} catch (error: any) {
if (error.response.status == 401) {
return {auth: 0, error: null};
}
return {auth: 0, error: error};
}
}

View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import { apiCall } from "../../api";
let rules = $ref([]);
async function loadRules(){
let res = await apiCall("Firewall.GetForwardRules", {});
if (res.Error === null) {
rules = res.Data.ForwardRules;
console.debug("rules", rules);
} else {
console.debug("error", res);
}
}
onMounted(async() => {
loadRules();
});
</script>
<template>
<div>
<PageHeader title="Forward Rules">
<button @click="loadRules">Load Rules</button>
</PageHeader>
<NiceTable :columns="{name: 'Name', verdict: 'Verdict'}" :sort-self="false" v-model:data="rules"/>
</div>
</template>

View file

@ -18,6 +18,9 @@ export default defineConfig({
server: {
"proxy": {
"/api": "http://localhost:8080",
"/login": "http://localhost:8080",
"/logout": "http://localhost:8080",
"/session": "http://localhost:8080",
"/ws": {
target: "ws://localhost:8080",
ws: true,

View file

@ -34,7 +34,7 @@ func main() {
os.Exit(1)
}
slog.Info("Validating Config")
slog.Info("Validating Config...")
if *applyPtr {
slog.Info("Applying Config...")
@ -54,7 +54,7 @@ func main() {
slog.Info("Starting Webserver...")
server.StartWebserver(conf, apiHandler)
slog.Info("Ready")
slog.Info("Ready.")
// Handle Exit Signal
sigChan := make(chan os.Signal, 1)

View file

@ -14,7 +14,6 @@ type Config struct {
func ValidateConfig(conf *Config) error {
val := validator.New()
slog.Info("Registering validator")
val.RegisterValidation("test", nilIfOtherNil)
return val.Struct(conf)
}

View file

@ -10,6 +10,7 @@ import (
"runtime/debug"
"golang.org/x/exp/slog"
"nfsense.net/nfsense/pkg/session"
)
type Handler struct {
@ -25,7 +26,7 @@ func NewHandler(maxRequestSize int64) *Handler {
}
}
func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) error {
func (h *Handler) HandleRequest(ctx context.Context, s *session.Session, r io.Reader, w io.Writer) error {
defer func() {
if r := recover(); r != nil {
slog.Error("Recovered Panic Handling JSONRPC Request", fmt.Errorf("%v", r), "stack", debug.Stack())
@ -45,13 +46,17 @@ func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) e
dec.DisallowUnknownFields()
err = dec.Decode(&req)
if err != nil {
return respondError(w, "", ErrParse, fmt.Errorf("Parsing Request: %w", err))
return respondError(w, "", ErrParse, fmt.Errorf("Decodeing Request: %w", err))
}
if req.Jsonrpc != "2.0" {
return respondError(w, req.ID, ErrMethodNotFound, fmt.Errorf("Unsupported Jsonrpc version %v", req.Jsonrpc))
}
if s == nil {
return respondError(w, req.ID, 401, fmt.Errorf("Unauthorized"))
}
method, ok := h.methods[req.Method]
if !ok {
return respondError(w, req.ID, ErrMethodNotFound, fmt.Errorf("Unknown Method %v", req.Method))
@ -60,11 +65,13 @@ func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) e
p := reflect.New(method.inType)
paramPointer := p.Interface()
if len(req.Params) != 0 {
dec = json.NewDecoder(bytes.NewReader(req.Params))
dec.DisallowUnknownFields()
err = dec.Decode(paramPointer)
if err != nil {
return respondError(w, req.ID, ErrInvalidParams, fmt.Errorf("Parsing Request: %w", err))
return respondError(w, req.ID, ErrInvalidParams, fmt.Errorf("Decoding Parameters: %w", err))
}
}
params := make([]reflect.Value, 3)

View file

@ -8,14 +8,17 @@ import (
"time"
"golang.org/x/exp/slog"
"nfsense.net/nfsense/pkg/session"
)
func HandleAPI(w http.ResponseWriter, r *http.Request) {
_, s := GetSession(r)
slog.Info("Api Handler hit")
_, s := session.GetSession(r)
if s == nil {
// Fallthrough after so that jsonrpc can still deliver a valid jsonrpc error
w.WriteHeader(http.StatusUnauthorized)
return
}
defer func() {
if r := recover(); r != nil {
slog.Error("Recovered Panic Handling HTTP API Request", fmt.Errorf("%v", r), "stack", debug.Stack())
@ -23,10 +26,10 @@ func HandleAPI(w http.ResponseWriter, r *http.Request) {
return
}
}()
ctx, cancel := context.WithTimeout(context.WithValue(r.Context(), SessionKey, s), time.Second*10)
ctx, cancel := context.WithTimeout(context.WithValue(r.Context(), session.SessionKey, s), time.Second*10)
defer cancel()
err := apiHandler.HandleRequest(ctx, r.Body, w)
err := apiHandler.HandleRequest(ctx, s, r.Body, w)
if err != nil {
slog.Error("Handling HTTP API Request", err)
}

View file

@ -10,6 +10,7 @@ import (
"nfsense.net/nfsense/pkg/definitions"
"nfsense.net/nfsense/pkg/jsonrpc"
"nfsense.net/nfsense/pkg/session"
)
var server http.Server
@ -32,7 +33,7 @@ func StartWebserver(conf *definitions.Config, _apiHandler *jsonrpc.Handler) {
stopCleanup = make(chan struct{})
go CleanupSessions(stopCleanup)
go session.CleanupSessions(stopCleanup)
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {

View file

@ -1,98 +1,63 @@
package server
import (
"encoding/json"
"io"
"net/http"
"sync"
"time"
"github.com/google/uuid"
"golang.org/x/exp/slog"
"nfsense.net/nfsense/pkg/session"
)
type SessionKeyType string
const SessionKey SessionKeyType = "session"
type Session struct {
Username string
Expires time.Time
}
var sessionsSync sync.Mutex
var sessions map[string]*Session = map[string]*Session{}
func GetSession(r *http.Request) (string, *Session) {
c, err := r.Cookie("session")
if err != nil {
return "", nil
}
s, ok := sessions[c.Value]
if ok {
return c.Value, s
}
return "", nil
}
func GenerateSession(w http.ResponseWriter, username string) {
id := uuid.New().String()
expires := time.Now().Add(time.Minute * 5)
sessionsSync.Lock()
defer sessionsSync.Unlock()
sessions[id] = &Session{
Username: username,
Expires: expires,
}
http.SetCookie(w, &http.Cookie{Name: "session", HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: expires})
}
func CleanupSessions(stop chan struct{}) {
tick := time.NewTicker(time.Minute)
for {
select {
case <-tick.C:
ids := []string{}
sessionsSync.Lock()
for id, s := range sessions {
if time.Now().After(s.Expires) {
ids = append(ids, id)
}
}
for _, id := range ids {
delete(sessions, id)
}
sessionsSync.Unlock()
case <-stop:
return
}
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
func HandleLogin(w http.ResponseWriter, r *http.Request) {
username := r.PostFormValue("username")
password := r.PostFormValue("password")
if username == "admin" && password == "12345" {
GenerateSession(w, username)
buf, err := io.ReadAll(r.Body)
if err != nil {
slog.Error("Reading Body", err)
return
}
var req LoginRequest
err = json.Unmarshal(buf, &req)
if err != nil {
slog.Error("Unmarshal", err)
return
}
if req.Username == "admin" && req.Password == "12345" {
slog.Info("User Login Successfull")
session.GenerateSession(w, req.Username)
w.WriteHeader(http.StatusOK)
http.Redirect(w, r, "/", http.StatusFound)
return
}
w.WriteHeader(http.StatusUnauthorized)
}
func HandleLogout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "session", Value: "", Expires: time.Now()})
http.SetCookie(w, session.GetCookie("", time.Now()))
w.WriteHeader(http.StatusOK)
}
func HandleSession(w http.ResponseWriter, r *http.Request) {
id, s := GetSession(r)
id, s := session.GetSession(r)
if s == nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
sessionsSync.Lock()
defer sessionsSync.Unlock()
if s != nil {
s.Expires = time.Now().Add(time.Minute * 5)
}
http.SetCookie(w, &http.Cookie{Name: "session", HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: s.Expires})
session.ExtendSession(s)
http.SetCookie(w, session.GetCookie(id, s.Expires))
w.WriteHeader(http.StatusOK)
resp := session.SessionResponse{
CommitHash: session.CommitHash,
}
res, err := json.Marshal(resp)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(res)
}

View file

@ -9,17 +9,18 @@ import (
"time"
"golang.org/x/exp/slog"
"nfsense.net/nfsense/pkg/session"
"nhooyr.io/websocket"
)
func HandleWebsocketAPI(w http.ResponseWriter, r *http.Request) {
_, s := GetSession(r)
_, s := session.GetSession(r)
if s == nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
ctx, cancel := context.WithCancel(context.WithValue(r.Context(), SessionKey, s))
ctx, cancel := context.WithCancel(context.WithValue(r.Context(), session.SessionKey, s))
defer cancel()
c, err := websocket.Accept(w, r, nil)
if err != nil {
@ -51,7 +52,7 @@ func HandleWebsocketAPI(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
err := apiHandler.HandleRequest(ctx, bytes.NewReader(m), w)
err := apiHandler.HandleRequest(ctx, s, bytes.NewReader(m), w)
if err != nil {
slog.Error("Handling Websocket API Request", err)
}

10
pkg/session/cookie.go Normal file
View file

@ -0,0 +1,10 @@
package session
import (
"net/http"
"time"
)
func GetCookie(value string, expires time.Time) *http.Cookie {
return &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: value, Expires: expires}
}

93
pkg/session/session.go Normal file
View file

@ -0,0 +1,93 @@
package session
import (
"net/http"
"runtime/debug"
"sync"
"time"
"github.com/google/uuid"
)
type SessionKeyType string
const SessionKey SessionKeyType = "session"
const SessionCookieName string = "session"
type Session struct {
Username string
Expires time.Time
// TODO Add []websocket.Conn pointer to close all active websockets, alternativly do this via context cancelation
}
type SessionResponse struct {
CommitHash string `json:"commit_hash"`
}
var sessionsSync sync.Mutex
var sessions map[string]*Session = map[string]*Session{}
var CommitHash = func() string {
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
return setting.Value
}
}
}
return "asd"
}()
func ExtendSession(s *Session) {
sessionsSync.Lock()
defer sessionsSync.Unlock()
if s != nil {
s.Expires = time.Now().Add(time.Minute * 5)
}
}
func GetSession(r *http.Request) (string, *Session) {
c, err := r.Cookie("session")
if err != nil {
return "", nil
}
s, ok := sessions[c.Value]
if ok {
return c.Value, s
}
return "", nil
}
func GenerateSession(w http.ResponseWriter, username string) {
id := uuid.New().String()
expires := time.Now().Add(time.Minute * 5)
sessionsSync.Lock()
defer sessionsSync.Unlock()
sessions[id] = &Session{
Username: username,
Expires: expires,
}
http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: expires})
}
func CleanupSessions(stop chan struct{}) {
tick := time.NewTicker(time.Minute)
for {
select {
case <-tick.C:
ids := []string{}
sessionsSync.Lock()
for id, s := range sessions {
if time.Now().After(s.Expires) {
ids = append(ids, id)
}
}
for _, id := range ids {
delete(sessions, id)
}
sessionsSync.Unlock()
case <-stop:
return
}
}
}