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 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** - off, debug, info, warn or error. Default info. Push to stream used by remote real-time log subscription
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
- - using browser and url: https://host/logger
750
- - curl -Ns https://host/logger -u gwread:password | sed 's/\xE2\x80\x8B//g'
751
- - curl -Ns https://host/logger -H "Authorization: Bearer secret" | sed 's/\xE2\x80\x8B//g'
752
- (-s and sed to ignore keep-alive character)
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
- // startup: bun <scriptname.ts>
796
- // update url (ws or wss) and the auth according to environment used
797
- const url = 'ws://localhost:8880/logger'
798
- const auth = 'Basic ' + btoa('gwadmin' + ':' + 'password') // const auth = 'Bearer ' + 'secret'
799
-
800
- const tls: any = {}
801
- if (url.startsWith('wss:')) {
802
- tls.ca = [Bun.file('/path/to/self-signed-cert.pem')], // only needed for self-signed certs
803
- tls.rejectUnauthorized = false
804
- }
805
-
806
- // messageHandler implements message handling and custom logic
807
- // could also use JSON.parse(message) and granular filtering on log "level"
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
- const startWebSocket = async () => {
813
- try {
814
- const ws = new WebSocket(url, {
815
- headers: {
816
- Authorization: auth,
817
- },
818
- tls,
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 retry = async () => {
850
- console.log('🔁 Retry in 10 seconds...')
851
- await Bun.sleep(10 * 1000)
852
- startWebSocket()
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
- startWebSocket()
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]
@@ -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(o?.connection?.options, options)
581
+ options = utils.extendObj(options, o)
582
582
  }
583
583
 
584
584
  const cli: any = {}
@@ -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(`\u200B`))
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(`${JSON.stringify(msgObj)}\n`))
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(`\u200B`)) // invisible keep-alive
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 wssInit = `
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 style="margin:0;">SCIM Gateway remote logger</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 ws = new WebSocket('{{protocol}}//' + location.host + location.pathname)
2739
- ws.onmessage = function(event) {
2740
- event.data.split('\\n').forEach(function(line) {
2741
- if (!line.trim()) return
2742
- const htmlLine = line.replace(
2743
- /(level":"\\s*)(debug|info|warn|error)/i,
2744
- function(match, p1, p2) {
2745
- let color = ''
2746
- switch (p2.toLowerCase()) {
2747
- case 'debug': color = '#888'; break
2748
- case 'info': color = 'blue'; break
2749
- case 'warn': color = 'orange'; break
2750
- case 'error': color = 'red'; break
2751
- default: color = 'black'
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
- logElem.innerHTML += htmlLine + '<br>'
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 (ws) {
2761
- ws.close()
2762
- ws = null
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.get('upgrade')?.toLowerCase() === 'websocket') { // browser step 2, and other Bun ws(s) clients
2811
- logger.info(`${gwName}[${pluginName}] remote logger connected from ip address ${ctx.ip}`, { baseEntity: ctx?.routeObj?.baseEntity })
2812
- return server.upgrade(req, { // after upgrade, the server will handle the WebSocket connection configured in Bun.serve()
2813
- data: { // passed to WebSocket server Bun open handler
2814
- headers: req.headers,
2815
- url: req.url,
2816
- baseEntity: ctx?.routeObj?.baseEntity,
2817
- ip: ctx.ip,
2818
- nonce: this.Nonce.createItem(crypto.randomUUID()),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.4.2",
3
+ "version": "5.4.3",
4
4
  "type": "module",
5
5
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
6
6
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",