scimgateway 5.4.2 → 5.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -58
- package/lib/helper-rest.ts +1 -1
- package/lib/scimgateway.ts +66 -93
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -394,7 +394,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
394
394
|
|
|
395
395
|
- **log.loglevel.console** - off, debug, info, warn or error. Default off. Output to stdout and errors to stderr
|
|
396
396
|
|
|
397
|
-
- **log.loglevel.push** -
|
|
397
|
+
- **log.loglevel.push** - debug, info, warn or error. Default info. Push to stream used by remote real-time log subscription
|
|
398
398
|
|
|
399
399
|
- **log.logDirectory** - custom defined log directory e.g. `/var/log/scimgateway` that will override default `<scimgateway path>/logs`. If not exist it will be created.
|
|
400
400
|
|
|
@@ -746,13 +746,18 @@ Please see code editor method HelperRest doRequest() IntelliSense for type and o
|
|
|
746
746
|
### Configuration notes - Remote real-time log subscription
|
|
747
747
|
Using remote real-time log subscription we may implement custom logic like monitoring and centralized logging
|
|
748
748
|
|
|
749
|
-
-
|
|
750
|
-
- curl -
|
|
751
|
-
|
|
752
|
-
|
|
749
|
+
- browser and url: https://host/logger
|
|
750
|
+
- curl with -u or -H "Authorization: Bearer secret"
|
|
751
|
+
```
|
|
752
|
+
curl -Ns http://localhost:8880/logger -u gwadmin:password | awk '
|
|
753
|
+
/^data: / {sub(/^data: /,""); printf "%s", $0; last=1; next}
|
|
754
|
+
/^$/ {if (last) print ""; last=0}
|
|
755
|
+
'
|
|
756
|
+
```
|
|
753
757
|
- custom client API (see example below)
|
|
754
758
|
- not supported by Azure Relay
|
|
755
759
|
|
|
760
|
+
|
|
756
761
|
We may configure read-only user/secret for log collection purpose
|
|
757
762
|
|
|
758
763
|
"auth": {
|
|
@@ -792,67 +797,57 @@ Example using debug loglevel:
|
|
|
792
797
|
Example code implementing remote real-time log subscription and custom message handling
|
|
793
798
|
|
|
794
799
|
```
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
800
|
+
//
|
|
801
|
+
// usage: bun <scriptname.ts>
|
|
802
|
+
// update url and the auth according to environment used
|
|
803
|
+
//
|
|
804
|
+
const username = "gwadmin"
|
|
805
|
+
const password = "password"
|
|
806
|
+
const url = "http://localhost:8880/logger"
|
|
807
|
+
|
|
808
|
+
const headers = new Headers({
|
|
809
|
+
Authorization: "Basic " + btoa(`${username}:${password}`),
|
|
810
|
+
Accept: "text/event-stream"
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
// message handling and custom logic
|
|
814
|
+
// we could also do JSON.parse(message) and granular filtering on log "level"
|
|
808
815
|
const messageHandler = async (message: string) => {
|
|
809
816
|
console.log(message)
|
|
810
817
|
}
|
|
811
818
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
headers
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// message is received
|
|
822
|
-
ws.addEventListener("message", event => {
|
|
823
|
-
messageHandler(event.data)
|
|
824
|
-
})
|
|
825
|
-
|
|
826
|
-
// socket opened
|
|
827
|
-
ws.addEventListener("open", event => {
|
|
819
|
+
async function startSSE() {
|
|
820
|
+
while (true) {
|
|
821
|
+
try {
|
|
822
|
+
const resp = await fetch(url, { headers });
|
|
823
|
+
if (!resp.ok || !resp.body) {
|
|
824
|
+
console.error(`❌ Response error: ${resp.status} ${resp.statusText}`)
|
|
825
|
+
await Bun.sleep(10_000)
|
|
826
|
+
continue
|
|
827
|
+
}
|
|
828
828
|
console.log('✅ Now awaiting log events...\n')
|
|
829
|
-
})
|
|
830
|
-
|
|
831
|
-
// socket closed
|
|
832
|
-
ws.addEventListener("close", event => {
|
|
833
|
-
let addInfo = ''
|
|
834
|
-
if (event.code === 1002) addInfo = ' => most likely authentication failure?'
|
|
835
|
-
console.warn(`⚠️ Connection closed (${event.code}): ${event.reason || 'no reason'}${addInfo}`)
|
|
836
|
-
retry()
|
|
837
|
-
})
|
|
838
|
-
|
|
839
|
-
// error handler
|
|
840
|
-
ws.addEventListener("error", event => {
|
|
841
|
-
// console.error('❌ WebSocket error:', event.message)
|
|
842
|
-
})
|
|
843
|
-
|
|
844
|
-
} catch (err: any) {
|
|
845
|
-
console.error('❌ Unexpected error:', err)
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
829
|
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
830
|
+
const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader()
|
|
831
|
+
|
|
832
|
+
while (true) {
|
|
833
|
+
const { value, done } = await reader.read()
|
|
834
|
+
if (done) break
|
|
835
|
+
if (!value.startsWith('data: ')) continue
|
|
836
|
+
const i = value.indexOf("\n\n")
|
|
837
|
+
if (i < 1) continue
|
|
838
|
+
const msg = value.slice(6, i)
|
|
839
|
+
messageHandler(msg)
|
|
840
|
+
}
|
|
841
|
+
console.error("⚠️ Connection closed");
|
|
842
|
+
await Bun.sleep(10_000)
|
|
843
|
+
} catch (err: any) {
|
|
844
|
+
console.error("❌ Connection error:", err?.message || err)
|
|
845
|
+
await Bun.sleep(10_000)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
853
848
|
}
|
|
854
849
|
|
|
855
|
-
|
|
850
|
+
startSSE()
|
|
856
851
|
```
|
|
857
852
|
|
|
858
853
|
### Configuration notes - Azure Relay
|
|
@@ -1473,6 +1468,16 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1473
1468
|
|
|
1474
1469
|
## Change log
|
|
1475
1470
|
|
|
1471
|
+
### v5.4.3
|
|
1472
|
+
|
|
1473
|
+
[Fixed]
|
|
1474
|
+
|
|
1475
|
+
- helper-rest, fixed an issue introduced in v5.3.8 that caused problems using OAuth
|
|
1476
|
+
|
|
1477
|
+
[Improved]
|
|
1478
|
+
|
|
1479
|
+
- Remote real-time logger
|
|
1480
|
+
|
|
1476
1481
|
### v5.4.2
|
|
1477
1482
|
|
|
1478
1483
|
[Improved]
|
package/lib/helper-rest.ts
CHANGED
|
@@ -578,7 +578,7 @@ export class HelperRest {
|
|
|
578
578
|
options.headers['Authorization'] = 'Basic ' + Buffer.from(`${o.auth?.options?.username}:${o.auth?.options?.password}`).toString('base64')
|
|
579
579
|
delete o.auth
|
|
580
580
|
}
|
|
581
|
-
options = utils.extendObj(
|
|
581
|
+
options = utils.extendObj(options, o)
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
const cli: any = {}
|
package/lib/scimgateway.ts
CHANGED
|
@@ -878,23 +878,24 @@ export class ScimGateway {
|
|
|
878
878
|
const getHandlerLoggerSSE = async (ctx: Context) => {
|
|
879
879
|
const levelInt = logger.levelToInt(this.config?.scimgateway?.log?.loglevel?.push || 'info')
|
|
880
880
|
const encoder = new TextEncoder()
|
|
881
|
+
logger.info(`${gwName}[${pluginName}] remote logger connected from ip address ${ctx.ip}`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
881
882
|
|
|
882
883
|
return new Response(
|
|
883
884
|
new ReadableStream({
|
|
884
885
|
start(controller) {
|
|
885
|
-
controller.enqueue(encoder.encode(
|
|
886
|
+
controller.enqueue(encoder.encode(`: keep-alive\n\n`))
|
|
886
887
|
|
|
887
888
|
const sub = async (msgObj: Record<string, any>) => {
|
|
888
889
|
if (logger.levelToInt(msgObj.level) < levelInt) return
|
|
889
890
|
if (ctx?.routeObj?.baseEntity !== 'undefined') { // if using baseEntity e.g. <host>/company1/logger, only include corresponding baseEntity logentries
|
|
890
891
|
if (ctx?.routeObj?.baseEntity !== msgObj.baseEntity) return
|
|
891
892
|
}
|
|
892
|
-
controller.enqueue(encoder.encode(
|
|
893
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(msgObj)}\n\n`))
|
|
893
894
|
}
|
|
894
895
|
logger.subscribe(sub)
|
|
895
896
|
|
|
896
897
|
const keepAliveInterval = setInterval(() => {
|
|
897
|
-
controller.enqueue(encoder.encode(
|
|
898
|
+
controller.enqueue(encoder.encode(`: keep-alive\n\n`))
|
|
898
899
|
}, 10000)
|
|
899
900
|
|
|
900
901
|
const cleanup = () => {
|
|
@@ -2705,15 +2706,39 @@ export class ScimGateway {
|
|
|
2705
2706
|
const isPublisherEnabled = this.config.scimgateway.stream.publisher.enabled
|
|
2706
2707
|
const isChainingEnabled = this.config.scimgateway.chainingBaseUrl
|
|
2707
2708
|
|
|
2708
|
-
const
|
|
2709
|
+
const sseInit = `
|
|
2709
2710
|
<!DOCTYPE html>
|
|
2710
2711
|
<html>
|
|
2711
2712
|
<head>
|
|
2712
2713
|
<style>
|
|
2714
|
+
html, body {
|
|
2715
|
+
height: 100%;
|
|
2716
|
+
margin: 0;
|
|
2717
|
+
padding: 0;
|
|
2718
|
+
}
|
|
2719
|
+
body {
|
|
2720
|
+
display: flex;
|
|
2721
|
+
flex-direction: column;
|
|
2722
|
+
height: 100vh;
|
|
2723
|
+
margin-left: 8px;
|
|
2724
|
+
}
|
|
2713
2725
|
.header-flex {
|
|
2714
2726
|
display: flex;
|
|
2715
2727
|
align-items: center;
|
|
2716
2728
|
gap: 16px;
|
|
2729
|
+
flex-shrink: 0;
|
|
2730
|
+
margin-top: 2px;
|
|
2731
|
+
margin-bottom: 2px;
|
|
2732
|
+
}
|
|
2733
|
+
#log {
|
|
2734
|
+
flex: 1 1 auto;
|
|
2735
|
+
width: 100%;
|
|
2736
|
+
overflow: auto;
|
|
2737
|
+
white-space: pre;
|
|
2738
|
+
margin: 0;
|
|
2739
|
+
min-height: 0;
|
|
2740
|
+
height: auto;
|
|
2741
|
+
box-sizing: border-box;
|
|
2717
2742
|
}
|
|
2718
2743
|
#stopBtn {
|
|
2719
2744
|
padding: 4px 18px;
|
|
@@ -2728,42 +2753,41 @@ export class ScimGateway {
|
|
|
2728
2753
|
</head>
|
|
2729
2754
|
<body>
|
|
2730
2755
|
<div class="header-flex">
|
|
2731
|
-
<h3
|
|
2756
|
+
<h3>SCIM Gateway remote logger</h3>
|
|
2732
2757
|
<button id="stopBtn" type="button">Stop</button>
|
|
2733
2758
|
</div>
|
|
2734
2759
|
<pre id="log"></pre>
|
|
2735
2760
|
<script>
|
|
2736
2761
|
const stopBtn = document.getElementById('stopBtn')
|
|
2737
2762
|
const logElem = document.getElementById('log')
|
|
2738
|
-
let
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
}
|
|
2753
|
-
return p1 + '<span style="color:' + color + ';font-weight:bold">' + p2 + '</span>'
|
|
2763
|
+
let es = new EventSource(location.pathname)
|
|
2764
|
+
|
|
2765
|
+
es.onmessage = function(event) {
|
|
2766
|
+
if (!event.data.trim()) return
|
|
2767
|
+
const htmlLine = event.data.replace(
|
|
2768
|
+
/(level":"\s*)(debug|info|warn|error)/i,
|
|
2769
|
+
function(match, p1, p2) {
|
|
2770
|
+
let color = ''
|
|
2771
|
+
switch (p2.toLowerCase()) {
|
|
2772
|
+
case 'debug': color = '#888'; break
|
|
2773
|
+
case 'info': color = 'blue'; break
|
|
2774
|
+
case 'warn': color = 'orange'; break
|
|
2775
|
+
case 'error': color = 'red'; break
|
|
2776
|
+
default: color = 'black'
|
|
2754
2777
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2778
|
+
return p1 + '<span style="color:' + color + ';font-weight:bold">' + p2 + '</span>'
|
|
2779
|
+
}
|
|
2780
|
+
)
|
|
2781
|
+
logElem.innerHTML += htmlLine + '<br>'
|
|
2782
|
+
logElem.scrollTop = logElem.scrollHeight
|
|
2758
2783
|
}
|
|
2784
|
+
|
|
2759
2785
|
stopBtn.onclick = function() {
|
|
2760
|
-
if (
|
|
2761
|
-
|
|
2762
|
-
|
|
2786
|
+
if (es) {
|
|
2787
|
+
es.close()
|
|
2788
|
+
es = null
|
|
2763
2789
|
stopBtn.textContent = 'Start'
|
|
2764
|
-
stopBtn.onclick = function() {
|
|
2765
|
-
location.reload()
|
|
2766
|
-
}
|
|
2790
|
+
stopBtn.onclick = function() { location.reload() }
|
|
2767
2791
|
}
|
|
2768
2792
|
}
|
|
2769
2793
|
</script>
|
|
@@ -2807,29 +2831,18 @@ export class ScimGateway {
|
|
|
2807
2831
|
await getHandlerServiceProviderConfig(ctx)
|
|
2808
2832
|
return await onAfterHandle(ctx)
|
|
2809
2833
|
case 'GET logger': // no onAfterHandle
|
|
2810
|
-
if (req.headers.
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
if (req.headers.has('sec-fetch-dest') && typeof Bun !== 'undefined') { // client is browser and not supporting WebSocket on Node.js
|
|
2823
|
-
const url = new URL(ctx.origin)
|
|
2824
|
-
const protocol = (url.protocol === 'https:' ? 'wss:' : 'ws:')
|
|
2825
|
-
const js = wssInit.replace('{{protocol}}', protocol)
|
|
2826
|
-
return new Response(js, { // browser step 1 => force WebSocket by sending javascript
|
|
2827
|
-
status: 200,
|
|
2828
|
-
headers: {
|
|
2829
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
2830
|
-
},
|
|
2831
|
-
})
|
|
2832
|
-
} else return await getHandlerLoggerSSE(ctx) // using SSE for none WebSocket/wss e.g. curl -Ns http://localhost:8880/logger -u gwadmin:password | sed 's/\xE2\x80\x8B//g'
|
|
2834
|
+
if (req.headers.has('sec-fetch-dest')) { // client is browser
|
|
2835
|
+
if (ctx.request.headers.get('accept')?.includes('text/event-stream')) {
|
|
2836
|
+
return await getHandlerLoggerSSE(ctx)
|
|
2837
|
+
} else {
|
|
2838
|
+
return new Response(sseInit, {
|
|
2839
|
+
status: 200,
|
|
2840
|
+
headers: {
|
|
2841
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
2842
|
+
},
|
|
2843
|
+
})
|
|
2844
|
+
}
|
|
2845
|
+
} else return await getHandlerLoggerSSE(ctx)
|
|
2833
2846
|
case 'PATCH users':
|
|
2834
2847
|
case 'PATCH groups':
|
|
2835
2848
|
await patchHandler(ctx)
|
|
@@ -2884,49 +2897,9 @@ export class ScimGateway {
|
|
|
2884
2897
|
const reqWithRaw = req as Request & { raw: IncomingMessage }
|
|
2885
2898
|
return await route(reqWithRaw, srv.requestIP(req)?.address || '')
|
|
2886
2899
|
},
|
|
2887
|
-
websocket: {
|
|
2888
|
-
open: (ws) => {
|
|
2889
|
-
const data = ws.data as { headers: Headers, url: string, baseEntity: string, ip: string, nonce: string } || {}
|
|
2890
|
-
let isAuthorized = false // client is already authenticated by initial http/https upgrade to websocket, anyhow passing data to be validated
|
|
2891
|
-
if (data?.nonce && this.Nonce.isItemValid(data.nonce)) {
|
|
2892
|
-
if (data.headers.has('authorization')) {
|
|
2893
|
-
if (data.url.endsWith('/logger')) isAuthorized = true
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
if (!isAuthorized) {
|
|
2897
|
-
logger.error(`${gwName}[${pluginName}] remote logger ip address ${data.ip} - WebSocket connection error: invalid nonce`, { baseEntity: data.baseEntity })
|
|
2898
|
-
ws.close(3000, 'Unauthorized')
|
|
2899
|
-
return
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
const levelInt = logger.levelToInt(this.config?.scimgateway?.log?.loglevel?.push || 'info')
|
|
2903
|
-
const sub = async (msgObj: Record<string, any>) => {
|
|
2904
|
-
if (logger.levelToInt(msgObj.level) < levelInt) return
|
|
2905
|
-
if (data.baseEntity !== 'undefined') { // if using baseEntity e.g. <host>/company1/logger, only include corresponding baseEntity logentries
|
|
2906
|
-
if (data.baseEntity !== msgObj.baseEntity) return
|
|
2907
|
-
}
|
|
2908
|
-
ws.send(`${JSON.stringify(msgObj)}`)
|
|
2909
|
-
}
|
|
2910
|
-
logger.subscribe(sub)
|
|
2911
|
-
;(ws as any)._sub = sub
|
|
2912
|
-
;(ws as any)._baseEntity = data.baseEntity
|
|
2913
|
-
;(ws as any)._ip = data.ip
|
|
2914
|
-
},
|
|
2915
|
-
close: (ws) => {
|
|
2916
|
-
const sub = (ws as any)._sub
|
|
2917
|
-
const baseEntity = (ws as any)._baseEntity
|
|
2918
|
-
const ip = (ws as any)._ip
|
|
2919
|
-
if (sub) {
|
|
2920
|
-
logger.unsubscribe(sub)
|
|
2921
|
-
}
|
|
2922
|
-
logger.info(`${gwName}[${pluginName}] remote logger disconnected from ip address ${ip}`, { baseEntity })
|
|
2923
|
-
},
|
|
2924
|
-
message: () => {},
|
|
2925
|
-
},
|
|
2926
2900
|
})
|
|
2927
2901
|
} else {
|
|
2928
2902
|
// using nodejs server either through Bun compability or Node.js
|
|
2929
|
-
|
|
2930
2903
|
// get body from req
|
|
2931
2904
|
async function getRequestBody(req: any): Promise<Buffer> {
|
|
2932
2905
|
return new Promise((resolve, reject) => {
|
package/package.json
CHANGED