diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index bd052cc..a0b04e1 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -47,7 +47,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [16.x, 18.x, 20.x] + node-version: [16.x, 18.x, 20.x, 22.x] os: [ubuntu-latest, windows-latest, macos-latest] steps: @@ -99,6 +99,9 @@ jobs: BODY="${BODY//$'\n'/'%0A'}" BODY="${BODY//$'\r'/'%0D'}" echo "::set-output name=BODY::$BODY" + if [[ $VERSION == *"-"* ]] ; then + echo "::set-output name=TAG::--tag next" + fi - name: Install Dependencies run: npm ci @@ -109,7 +112,7 @@ jobs: run: | npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} npm whoami - npm publish + npm publish ${{ steps.extract_release.outputs.TAG }} - name: Create Github Release id: create_release diff --git a/cert/cert.key b/cert/cert.key index 24b89a5..c340c42 100644 --- a/cert/cert.key +++ b/cert/cert.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCl2ofZKAqpOUtE -HOaedkYs3pv86P6CV3tRKXCV6BMoF0oj2HhYD6jxL92L0YyMjVi+Ysv4sWGjFB3L -UburXuDxKsU8EoksRFVFxRKRQFjtIO2tfnnkVWkdcyXUcnSgEBWfj8Gwt0Ouunxl -lS9OLUp/1zKaAk+mTU22nNkc/dy6D8REDj28UsAae4iuw5No4RYVNY28lvUbMLrP -fPVOhOMUU8cUvvrnpf7/9FkP8Sm2nG4vVpO6efYJ0wz7UiGmJUQQYRQEE/AZZSC0 -O7wwNBtkxY5JO6nnLZ3MoOPHXJkF1ptUikOBNh9Db46/DHIr0JJZSaACkLsKta8C -4MM74bTZAgMBAAECggEAJDAD3x5tARJmuURjD2U4F5c0yuCdk5v55LIZhiPeditq -ulqDm5dDIejzOows0ggPOm89GRS+/IGppJC/VXt8sjJGWb6jnnyEbJY84GN9Y7QB -GA9WEjuOlWXn9axJhRktVqTbuq6p/mhjNxjveuvH6w/t0wu5DEymrbcYakp7zD7F -c9Q6iLd7MNt+EGFJQ9HXIFfvd8aGC3lUWTfVIqhQ1YjBeQXHWuK8Z6FKj/ecJ6kV -VBD2pmTPvCS2oc5/TuEstfWkhUXbJkDJUx37SqOtp5F/N/IGUndOYZGcQM5QZ0dk -eQUzslFqxmiOur6ncYO0c9DdMS/iHbF7BOLJrLVG/wKBgQDWO4vdR5BGDyRADxm4 -avfdCi05lc7mAjD1ZKSzCZONOWXdKXtAk8W7UPDbTZtJ/nHXVui+gSMVSzpkqEvQ -gRs1E5m6lsp5NHwq6Q6/nrpn7aAlo9q2hBKXT6VZFkpqKQQQZfa/4Gb9KSCmNvoI -Dm62K+vx8BtWFUiXZMYIJca1MwKBgQDGMF3QG38VZehVPQpCTnr4SSCEx8x5p5sC -RJMO+mogDR43FW4Pn0olFNKqxVlV/SjBIQT0cupLq1oQjbmzQPoPk2fpBrKXMpvl -frUsm6mfEd4iBVLyLcn5NzBgOlkQrUtaz5w/L4lbDfDBZAksX8x4rGp4qI28lQuL -o3qLi7WVwwKBgD1Qe55Qbh1vFfvzlnPuwZQU5o61rqqr8+E39d98HSvtQpdC2RDJ -em07JERP+OL7nQ95w1FK2oSsrEDE3jYFzYiqXHRH1hlMiUEqxNrZDhbSruQ2+lEE -ieGenP9bXt71cEFVPYL7Md7BF6Qa1gLaRpuDBJuREfHYU5do8zi/vxh7AoGAD9S/ -OadooFnylBR7JE7Gjdyxh0m6cKFNxYGayaCBJ6xElJvWndLYhlvCdDetaiv9vGeZ -0Lj5NDAs0pOvmL0A/IuGyltpmqBFSbC0YirRAs7XkpogRQ4ZSxn4eEdQ4/8jvM5G -qdlvPGHBsIEAJpZEbANBwf+cysqREIdve4QebicCgYBSTgudClfJIObL/KhR9ET5 -yiJuye80x6cvGOFrkmI1XVRIykpIqtbvM32kBBN/2EmPVkOf9O79ohJnET4B6tTG -d2SMBjm/tBW6KhP0wQ8t69RWPMp3i/2HKGNqZvHQOKLbLTzEehNqnc616yx3d+UX -JM3DmzgTfECrhsicFzzoYA== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIOEfVJ4LFb3Dx +jbTgi/2giXy86Ty1zbIUCg43vaoM+6vSKfKwKSMW7gA01nsTRVuOzWCWsPhUdymy +UCZBWG2E1yI04LP30SpPqBaE9FfUN6dzI92C1bRe6t/iJxJLa1SgHcdjgSiRUo6Y +8xSzwd/TKNiRc3qxGGg/eSvYRasvg2QQC2arncmF4LI1yvz+dgrtRdoF1jr8NPXY +6bdSuAsGfMnmt74UH7Z7cDVfZsYuLVHMVW+fqxaxWr/2vLbygFcttEXxEx7Dspav +R36oX0MsI7yZCsxiVxXxO9ey8Ax4nes8/C2OISUnxH8EE9t6GvI9TyqETCrA4gzr +Pv1D18xjAgMBAAECggEAGSX14nsU/Nwzhn5IaCU36nslFMwC7nRIrXsEhXx9fdCl +Uz97lnmESR6vHfPhVO6oQmE02Zie3b/Il/djPAqcWaF1UB5NMeUEIyniB48dR7A5 +3cd1IjgvcKCgav/XV2vCybMT1pa5froCOTSHDCZfiQlKB8hGcooLRgsKi8lXEson +6/C9LPyUMUKPAM1+Dy90LE7iOz38vOhxEm7DNu0H7u0hXYbTTBldRj2I3e+cioR6 +DTbOMdkaDSyi7XGuKu6POaZmfsvsNPrRjc9oGochx+k5zOBZMj+GlvQ839mDBR7O +NwyKwX11GvQiF1kZwXriUxK6VrhsbGiQ160WOTuoCQKBgQD6E1r1Pa1ql9PLaKBc +MXkuVSgZC8S7pR+hON0BDqfy7suvAirvi8T+Q8+tb1SN27//qa2P/twpFaUivGTd +KYOzen2tZ6XQqd7O+Ybl6kfMXCEr7/4Boxlf09SNdxln7A9vs1hH01Je7XGJYIZk +wyB7lPFL1HzT0UZPlu0uOYrwbwKBgQDM9pAV8YnfOFAr+dyX99j2Oj7+H+4l/ATG +o7l67XGAeU+WxbwlQsj/Ms1ffnCU3qypuzaTd1KlzS11MKmnfRo4cSjNzdKDSK7T +6Zilnqc3e4CZaA7U+HZWjVs6jpJU74U0WwpFuOTGMb5a0kdXeQ9uOyd+yxdsbk7F +Zbd50WG1TQKBgBMmxlF/vrcqF3s9cQJ+e3RT6zU31II2XBzBuRMqpywQo6KsfNNJ +lfWPBemXXBddG/Adc4BSmVPAJ5xoZyUU19Q37kYIaQd46upY677R2VvKNnQh9gb0 +Ea5oD6Ah3d06k9gPGRSvF2DTuF03+jLfSq6MMoqHJGQoY8UWnuVqXLybAoGAc2pE +KJcis/fZ7Wl9tnVyTvTtk9wXFnybk9+OCpK6X0Xwc05VbAX3ePz6eNOSQcJCKDGr +wc5nU8X92wfUAOSJZ08RUxKbgCHlkJ7xvhFgx/VbrQbTk0l2GbbvsEGoVPurXpgF +aM18xb4tGqdeVPtunPvieZuTTROwd6eXcZleE+ECgYEAgHKVr8N9UDU3o3DLIdlR +GFLj8yct7Ui1YV3F2aYu+ihG4QHLsc6MAHkcaswNvzbyEHtHpyKHTMiIYcIGxdvi +bqFXDSQRowxXsgyRDWmnP6u2fEF68dSXlJ5BOtgqqF2OCaCjHnl+E7x28WeksZRJ +fYPhkGQ3UceMHd9oEWo//Uo= -----END PRIVATE KEY----- diff --git a/cert/cert.pem b/cert/cert.pem index ab50cce..3d7212d 100644 --- a/cert/cert.pem +++ b/cert/cert.pem @@ -1,24 +1,24 @@ -----BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIUcjAWwfDOv7FkmvXGaNs3oxLmsyIwDQYJKoZIhvcNAQEL -BQAwgY0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTERMA8GA1UEBwwITG9j +MIID8TCCAtmgAwIBAgIUTl+duPkIUXI1h7w2hiMbymkFUx8wDQYJKoZIhvcNAQEL +BQAwgYExCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTERMA8GA1UEBwwITG9j YXRpb24xGjAYBgNVBAoMEU9yZ2FuaXphdGlvbiBOYW1lMRwwGgYDVQQLDBNPcmdh -bml6YXRpb25hbCBVbml0MSEwHwYDVQQDDBhkYWlraW4tb25lY3RhMm1xdHQubG9j -YWwwHhcNMjQwNjI4MDc1MjEwWhcNMzQwNjI2MDc1MjEwWjCBjTELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVN0YXRlMREwDwYDVQQHDAhMb2NhdGlvbjEaMBgGA1UECgwR -T3JnYW5pemF0aW9uIE5hbWUxHDAaBgNVBAsME09yZ2FuaXphdGlvbmFsIFVuaXQx -ITAfBgNVBAMMGGRhaWtpbi1vbmVjdGEybXF0dC5sb2NhbDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKXah9koCqk5S0Qc5p52Rizem/zo/oJXe1EpcJXo -EygXSiPYeFgPqPEv3YvRjIyNWL5iy/ixYaMUHctRu6te4PEqxTwSiSxEVUXFEpFA -WO0g7a1+eeRVaR1zJdRydKAQFZ+PwbC3Q666fGWVL04tSn/XMpoCT6ZNTbac2Rz9 -3LoPxEQOPbxSwBp7iK7Dk2jhFhU1jbyW9Rswus989U6E4xRTxxS++uel/v/0WQ/x -Kbacbi9Wk7p59gnTDPtSIaYlRBBhFAQT8BllILQ7vDA0G2TFjkk7qectncyg48dc -mQXWm1SKQ4E2H0Nvjr8McivQkllJoAKQuwq1rwLgwzvhtNkCAwEAAaNrMGkwDgYD -VR0PAQH/BAQDAgOIMBMGA1UdJQQMMAoGCCsGAQUFBwMBMCMGA1UdEQQcMBqCGGRh -aWtpbi1vbmVjdGEybXF0dC5sb2NhbDAdBgNVHQ4EFgQUPxzrRNuFqDHzBmOnSv/t -IuL36mswDQYJKoZIhvcNAQELBQADggEBAClB4bQdPo50TlRODy/5WAWqq6i5HSqO -sylSvRJN6Qzcy4oiKTaUuUHR5yBL3mXzfiZc9LiSRDB3kTOzJv50Bm8S8dct3vFP -2WjMQm7bwk6yxYKZdVPqBXuAxzzW+4t5NkuepZp3lfbYS6tNNrnDnzdZBDKXWrB1 -1YUEi2mtTp0cFE3klMYfcJtUV+EgyjKMjbYcp2HFHYN2l2yV5jkH+Uik7pdzEfS+ -xWQ+v5TkVKcXCS+scDocAFz1r+mHJArycXfTv2BLpdmAWSFBrsEFl72BiIW4XZ+f -avgUUJSZcEW1yxpS3YXzq44MFmUJe6Xqpu8788Wl0xCJ3dLNRUHiL1w= +bml6YXRpb25hbCBVbml0MRUwEwYDVQQDDAxkYWlraW4ubG9jYWwwHhcNMjQwNzAy +MTAxNDI3WhcNMzQwNjMwMTAxNDI3WjCBgTELMAkGA1UEBhMCVVMxDjAMBgNVBAgM +BVN0YXRlMREwDwYDVQQHDAhMb2NhdGlvbjEaMBgGA1UECgwRT3JnYW5pemF0aW9u +IE5hbWUxHDAaBgNVBAsME09yZ2FuaXphdGlvbmFsIFVuaXQxFTATBgNVBAMMDGRh +aWtpbi5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMg4R9Un +gsVvcPGNtOCL/aCJfLzpPLXNshQKDje9qgz7q9Ip8rApIxbuADTWexNFW47NYJaw ++FR3KbJQJkFYbYTXIjTgs/fRKk+oFoT0V9Q3p3Mj3YLVtF7q3+InEktrVKAdx2OB +KJFSjpjzFLPB39Mo2JFzerEYaD95K9hFqy+DZBALZqudyYXgsjXK/P52Cu1F2gXW +Ovw09djpt1K4CwZ8yea3vhQftntwNV9mxi4tUcxVb5+rFrFav/a8tvKAVy20RfET +HsOylq9HfqhfQywjvJkKzGJXFfE717LwDHid6zz8LY4hJSfEfwQT23oa8j1PKoRM +KsDiDOs+/UPXzGMCAwEAAaNfMF0wDgYDVR0PAQH/BAQDAgOIMBMGA1UdJQQMMAoG +CCsGAQUFBwMBMBcGA1UdEQQQMA6CDGRhaWtpbi5sb2NhbDAdBgNVHQ4EFgQUtMiA +5rZoHyT32uu5Uw5WF9mUKXEwDQYJKoZIhvcNAQELBQADggEBAIH7BId2vX8XWs2A +qcBHAlGpt2GEdC0xSdGx/L70t/pFvYcwYLnTFYgPaDU/XzaBtwPSH71wTGTujjPF +GgDevYQxCU6/92vXO2GLgX2OAooD0+r53OO1H+dKMoRqpkX4IAqSMgZ7D5UxF76l +pG0N+bNu5TvkIXxLRojhpAmO1H82/hSRV6mObPUqL3rKHzMb5yGuCBXMbfqiF68e +QaKeHEuwBt9EiC+QJbu1GLVuh38Yih2v2TtfZQEkasisZqr7wjJSSCosZ04U56t0 +qbx3M8TrjsNWJB51WOGCzmWIqh+26DK2T8KXFmfJg2W+thygEItFAhXMCILhAOwQ +UpMJ67w= -----END CERTIFICATE----- diff --git a/cert/generate.sh b/cert/generate.sh old mode 100644 new mode 100755 diff --git a/cert/req.cnf b/cert/req.cnf index 9de5435..ad32fd1 100644 --- a/cert/req.cnf +++ b/cert/req.cnf @@ -7,11 +7,11 @@ C = US ST = State L = Location O = Organization Name -OU = Organizational Unit -CN = daikin-onecta2mqtt.local +OU = Organizational Unit +CN = daikin.local [v3_req] keyUsage = critical, digitalSignature, keyAgreement extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] -DNS.1 = daikin-onecta2mqtt.local \ No newline at end of file +DNS.1 = daikin.local diff --git a/package.json b/package.json index 661c047..6f17c5f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "url": "https://github.com/Apollon77/daikin-controller-cloud/issues" }, "scripts": { - "prepare": "npm run build", + "prepublish": "npm run build", "build": "tsc -p .", "release": "release-script", "build-tokensaver": "pkg example/tokensaver.js" diff --git a/src/device.ts b/src/device.ts index 1963d3e..c8ebcc1 100644 --- a/src/device.ts +++ b/src/device.ts @@ -1,12 +1,10 @@ - import type { OnectaClient } from './onecta/oidc-client.js'; /** * Class to represent and control one Daikin Cloud Device */ export class DaikinCloudDevice { - - client: OnectaClient; + #client: OnectaClient; desc: any; managementPoints: Record; @@ -14,21 +12,21 @@ export class DaikinCloudDevice { * Constructor, called from DaikinCloud class when initializing all devices * * @param deviceDescription object with device description from Cloud request - * @param cloudInstance Instance of DaikinCloud used for communication + * @param client Instance of DaikinCloud used for communication */ constructor(deviceDescription: any, client: OnectaClient) { this.managementPoints = {}; - this.client = client; + this.#client = client; this.setDescription(deviceDescription); } /** * Helper method to traverse the Device object returned by Daikin cloud for subPath datapoints * - * @param {object} obj Object to traverse - * @param {object} data Data object where all data are collected - * @param {string} [pathPrefix] remember the path when traversing through structure - * @returns {object} collected data + * @param obj Object to traverse + * @param data Data object where all data are collected + * @param [pathPrefix] remember the path when traversing through structure + * @returns collected data * @private */ _traverseDatapointStructure(obj: any, data?: any, pathPrefix?: string) { @@ -86,16 +84,16 @@ export class DaikinCloudDevice { /** * Get Daikin Device UUID - * @returns {string} Device Id (UUID) + * @returns Device Id (UUID) */ - getId() { + getId(): string { return this.desc.id; } /** * Get the original Daikin Device Description * - * @returns {object} Daikin Device Description + * @returns Daikin Device Description */ getDescription() { return this.desc; @@ -162,7 +160,7 @@ export class DaikinCloudDevice { */ async updateData() { // TODO: Enhance this method to also allow to get some partial data like only one managementPoint or such; needs checking how to request - const desc = await this.client.requestResource('/v1/gateway-devices/' + this.getId()); + const desc = await this.#client.requestResource('/v1/gateway-devices/' + this.getId()); this.setDescription(desc); return true; } @@ -237,9 +235,6 @@ export class DaikinCloudDevice { method: 'PATCH', body: JSON.stringify(setBody) } as const; - return this.client.requestResource(setPath, setOptions); + return this.#client.requestResource(setPath, setOptions); } - } - -module.exports = DaikinCloudDevice; diff --git a/src/example.ts b/src/example.ts index b90538a..64b0854 100644 --- a/src/example.ts +++ b/src/example.ts @@ -9,32 +9,35 @@ import { DaikinCloudController } from './index'; const { oidc_client_id, oidc_client_secret } = process.env; if (!oidc_client_id || !oidc_client_secret) { - console.log('Please set the oidc_client_id and oidc_client_secret environment variables'); - process.exit(0); + console.log('Please set the oidcClientId and oidcClientSecret environment variables'); + process.exit(0); } // ============================================================================ // Create a new instance of the Onecta API client. Note that the -// `oidc_callback_server_baseurl` **must** be set as the application's +// `oidcCallbackServerBaseUrl` **must** be set as the application's // "Redirect URI" within the Daikin Developer Portal. -// See https://developer.cloud.daikineurope.com . +// See https://developer.cloud.daikineurope.com . // ============================================================================ const controller = new DaikinCloudController({ - /* OIDC client id */ - oidc_client_id, - /* OIDC client secret */ - oidc_client_secret, - /* network interface that the HTTP server should bind to */ - oidc_callback_server_addr: '127.0.0.1', - /* port that the HTTP server should bind to */ - oidc_callback_server_port: 8765, - /* OIDC Redirect URI */ - oidc_callback_server_baseurl: 'https://daikin.local:8765', - /* path of file used to cache the OIDC tokenset */ - oidc_tokenset_file_path: resolve(homedir(), '.daikin-controller-cloud-tokenset'), - /* time to wait for the user to go through the authorization grant flow before giving up (in seconds) */ - oidc_authorization_timeout: 120, + /* OIDC client id */ + oidcClientId: oidc_client_id, + /* OIDC client secret */ + oidcClientSecret: oidc_client_secret, + /* Network interface that the HTTP server should bind to. Bind to all interfaces for convenience, please limit as needed to single interfaces! */ + oidcCallbackServerBindAddr: '0.0.0.0', + /* port that the HTTP server should bind to */ + oidcCallbackServerPort: 8765, + /* OIDC Redirect URI */ + oidcCallbackServerExternalAddress: 'daikin.local', + //oidcCallbackServerBaseUrl: 'https://daikin.local:8765', // or use local IP address where server is reachable + /* path of file used to cache the OIDC tokenset */ + oidcTokenSetFilePath: resolve(homedir(), '.daikin-controller-cloud-tokenset'), + /* time to wait for the user to go through the authorization grant flow before giving up (in seconds) */ + oidcAuthorizationTimeoutS: 120, + certificatePathKey: resolve(__dirname, '..', 'cert', 'cert.key'), + certificatePathCert: resolve(__dirname, '..', 'cert', 'cert.pem'), }); // ============================================================================ @@ -45,20 +48,25 @@ const controller = new DaikinCloudController({ // ============================================================================ controller.on('authorization_request', (url) => { - console.log('Please navigate to %s', url); + console.log(` +Please make sure that ${url} is set as "Redirect URL" in your Daikin Developer Portal account for the used Client! + +Then please open the URL ${url} in your browser and accept the security warning for the self signed certificate (if you open this for the first time). + +Afterwards you are redirected to Daikin to approve the access and then redirected back.`); }); (async () => { - // ========================================================================== - // OIDC authentication, authorization and token management are all abstracted - // away. The public methods exposed by the client map to the endpoints - // provided by the Onecta API. - // See https://developer.cloud.daikineurope.com/spec/b0dffcaa-7b51-428a-bdff-a7c8a64195c0/70b10aca-1b4c-470b-907d-56879784ea9c - // ========================================================================== + // ========================================================================== + // OIDC authentication, authorization and token management are all abstracted + // away. The public methods exposed by the client map to the endpoints + // provided by the Onecta API. + // See https://developer.cloud.daikineurope.com/spec/b0dffcaa-7b51-428a-bdff-a7c8a64195c0/70b10aca-1b4c-470b-907d-56879784ea9c + // ========================================================================== - const devices = await controller.getCloudDeviceDetails(); - console.log(devices); + const devices = await controller.getCloudDeviceDetails(); + console.log(devices); })().catch(console.error); diff --git a/src/index.ts b/src/index.ts index d6ed9a0..2b0ee2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,5 @@ - import { EventEmitter } from 'events'; import { DaikinCloudDevice } from './device.js'; - import { OnectaClient } from './onecta/oidc-client.js'; import { OnectaClientConfig } from './onecta/oidc-utils.js'; @@ -14,12 +12,11 @@ export declare interface DaikinCloudController { * Daikin Controller for Cloud solution to get tokens and interact with devices */ export class DaikinCloudController extends EventEmitter { - - private client: OnectaClient; + #client: OnectaClient; constructor(config: OnectaClientConfig) { super(); - this.client = new OnectaClient(config, this); + this.#client = new OnectaClient(config, this); } /** @@ -27,7 +24,7 @@ export class DaikinCloudController extends EventEmitter { * @returns {Promise} API Info object */ async getApiInfo() { - return this.client.requestResource('/v1/info'); + return this.#client.requestResource('/v1/info'); } /** @@ -35,7 +32,7 @@ export class DaikinCloudController extends EventEmitter { * @returns {Promise} pure Device details */ async getCloudDeviceDetails(): Promise { - return await this.client.requestResource('/v1/gateway-devices'); + return await this.#client.requestResource('/v1/gateway-devices'); } /** @@ -43,7 +40,6 @@ export class DaikinCloudController extends EventEmitter { */ async getCloudDevices(): Promise { const devices = await this.getCloudDeviceDetails(); - return devices.map(dev => new DaikinCloudDevice(dev, this.client)); + return devices.map(dev => new DaikinCloudDevice(dev, this.#client)); } - } diff --git a/src/onecta/oidc-callback-server.ts b/src/onecta/oidc-callback-server.ts index f0712fe..f09d5d7 100644 --- a/src/onecta/oidc-callback-server.ts +++ b/src/onecta/oidc-callback-server.ts @@ -1,6 +1,4 @@ - import type { IncomingMessage, ServerResponse } from 'node:http'; - import { resolve } from 'node:path'; import { createServer, Server } from 'node:https'; import { readFile } from 'node:fs/promises'; @@ -14,10 +12,10 @@ export type OnectaOIDCCallbackServerRequestListener< /** * Creates and starts a HTTPS server */ -export const startOnectaOIDCCallbackServer = async (config: OnectaClientConfig, oidc_state: string): Promise => { +export const startOnectaOIDCCallbackServer = async (config: OnectaClientConfig, oidc_state: string, auth_url: string): Promise => { const server = createServer({ - key: await readFile(resolve(__dirname, '..', '..', 'cert', 'cert.key')), - cert: await readFile(resolve(__dirname, '..', '..', 'cert', 'cert.pem')), + key: await readFile(config.certificatePathKey ?? resolve(__dirname, '..', '..', 'cert', 'cert.key')), + cert: await readFile(config.certificatePathCert ?? resolve(__dirname, '..', '..', 'cert', 'cert.pem')), }); await new Promise((resolve, reject) => { const cleanup = () => { @@ -34,7 +32,7 @@ export const startOnectaOIDCCallbackServer = async (config: OnectaClientConfig, }; server.on('listening', onListening); server.on('error', onError); - server.listen(config.oidc_callback_server_port, config.oidc_callback_server_addr); + server.listen(config.oidcCallbackServerPort, config.oidcCallbackServerBindAddr); }); return await new Promise((resolve, reject) => { let timeout: NodeJS.Timeout; @@ -58,19 +56,25 @@ export const startOnectaOIDCCallbackServer = async (config: OnectaClientConfig, resolve(code); }; const onRequest = (req: IncomingMessage, res: ServerResponse) => { - const url = new URL(req.url ?? '/', config.oidc_callback_server_baseurl); - const res_state = url.searchParams.get('state'); - const auth_code = url.searchParams.get('code'); - if (res_state === oidc_state && auth_code) { + const url = new URL(req.url ?? '/', config.oidcCallbackServerBaseUrl); + const resState = url.searchParams.get('state'); + const authCode = url.searchParams.get('code'); + if (resState === oidc_state && authCode) { res.statusCode = 200; - res.write(onecta_oidc_auth_thank_you_html); - res.once('finish', () => onAuthCode(auth_code)); - } else { + res.write(config.onectaOidcAuthThankYouHtml ?? onecta_oidc_auth_thank_you_html); + res.once('finish', () => onAuthCode(authCode)); + } else if (!resState && !authCode && (req.url ?? '/') === '/') { + //Redirect to auth_url + res.writeHead(302, { + 'Location': auth_url, + }); + } + else { res.statusCode = 400; } res.end(); }; - setTimeout(onTimeout, config.oidc_authorization_timeout * 1000); + setTimeout(onTimeout, config.oidcAuthorizationTimeoutS * 1000); server.on('request', onRequest); server.on('error', onError); }); diff --git a/src/onecta/oidc-client.ts b/src/onecta/oidc-client.ts index bed40e2..001cc8b 100644 --- a/src/onecta/oidc-client.ts +++ b/src/onecta/oidc-client.ts @@ -9,7 +9,6 @@ import { OnectaAPIBaseUrl, OnectaClientConfig, onecta_oidc_issuer, - onecta_oidc_auth_thank_you_html, } from './oidc-utils.js'; import { @@ -17,118 +16,123 @@ import { } from './oidc-callback-server.js'; export class OnectaClient { - - private _config: OnectaClientConfig; - private _client: BaseClient; - private _token_set: TokenSet | null; - private _emitter: EventEmitter; - private _get_token_set_queue: { resolve: (set: TokenSet) => any, reject: (err: Error) => any }[]; + #config: OnectaClientConfig; + #client: BaseClient; + #tokenSet: TokenSet | null; + #emitter: EventEmitter; + #getTokenSetQueue: { resolve: (set: TokenSet) => any, reject: (err: Error) => any }[]; constructor(config: OnectaClientConfig, emitter: EventEmitter) { - this._config = config; - this._emitter = emitter; - this._client = new onecta_oidc_issuer.Client({ - client_id: config.oidc_client_id, - client_secret: config.oidc_client_secret, + this.#config = config; + this.#emitter = emitter; + this.#client = new onecta_oidc_issuer.Client({ + client_id: config.oidcClientId, + client_secret: config.oidcClientSecret, }); - this._token_set = null; - this._get_token_set_queue = []; + this.#tokenSet = config.tokenSet ? new TokenSet(config.tokenSet) : null; + this.#getTokenSetQueue = []; + if (!config.oidcCallbackServerBaseUrl) { + config.oidcCallbackServerBaseUrl = + `https://${config.oidcCallbackServerExternalAddress ?? + config.oidcCallbackServerBindAddr}:${config.oidcCallbackServerPort}`; + } } - private async _authorize(): Promise { - const { _config, _client } = this; - const redirect_uri = _config.oidc_callback_server_baseurl; - const req_state = randomBytes(32).toString('hex'); - const auth_url = _client.authorizationUrl({ + async #authorize(): Promise { + const redirectUri = this.#config.oidcCallbackServerBaseUrl; + const reqState = randomBytes(32).toString('hex'); + const authUrl = this.#client.authorizationUrl({ scope: OnectaOIDCScope.basic, - state: req_state, - redirect_uri, + state: reqState, + redirect_uri: redirectUri, }); - this._emitter.emit('authorization_request', auth_url); - const auth_code = await startOnectaOIDCCallbackServer(this._config, req_state); - const token_set = await _client.grant({ + this.#emitter.emit('authorization_request', this.#config.oidcCallbackServerBaseUrl); + const authCode = + this.#config.customOidcCodeReceiver + ? await this.#config.customOidcCodeReceiver(authUrl, reqState) + : await startOnectaOIDCCallbackServer(this.#config, reqState, authUrl); + return await this.#client.grant({ grant_type: 'authorization_code', - client_id: _config.oidc_client_id, - client_secret: _config.oidc_client_secret, - code: auth_code, - redirect_uri, + client_id: this.#config.oidcClientId, + client_secret: this.#config.oidcClientSecret, + code: authCode, + redirect_uri: redirectUri, }); - return token_set; } - private async _refresh(refresh_token: string): Promise { - const { _client, _config } = this; - const token_set = await _client.grant({ + async #refresh(refreshToken: string): Promise { + return await this.#client.grant({ grant_type: 'refresh_token', - client_id: _config.oidc_client_id, - client_secret: _config.oidc_client_secret, - refresh_token, + client_id: this.#config.oidcClientId, + client_secret: this.#config.oidcClientSecret, + refreshToken, }); - return token_set; } - private async _loadTokenSet(): Promise { - const { _config } = this; - try { - const data = await readFile(_config.oidc_tokenset_file_path, 'utf8'); - const token_set = new TokenSet(JSON.parse(data)); - return token_set; - } catch (err) { - if ((err as { code?: string }).code !== 'ENOENT') { - this._emitter.emit('error', 'Could not load OIDC tokenset from disk: ' + (err as Error).message); + async #loadTokenSet(): Promise { + if (this.#config.oidcTokenSetFilePath) { + try { + const data = await readFile(this.#config.oidcTokenSetFilePath, 'utf8'); + return new TokenSet(JSON.parse(data)); + } catch (err) { + if ((err as { code?: string }).code !== 'ENOENT') { + this.#emitter.emit('error', 'Could not load OIDC tokenset from disk: ' + (err as Error).message); + } } - return null; } + return null; } - private async _storeTokenSet(set: TokenSet): Promise { - const { _config } = this; - try { - await writeFile(_config.oidc_tokenset_file_path, JSON.stringify(set, null, 2)); - } catch (err) { - this._emitter.emit('error', 'Could not store OIDC tokenset to disk: ' + (err as Error).message); + async #storeTokenSet(set: TokenSet): Promise { + this.#emitter.emit('token_update', set); + if (this.#config.oidcTokenSetFilePath) { + try { + await writeFile(this.#config.oidcTokenSetFilePath, JSON.stringify(set, null, 2)); + } catch (err) { + this.#emitter.emit('error', 'Could not store OIDC tokenset to disk: ' + (err as Error).message); + } } }; - private async _getTokenSet(): Promise { - let token_set: TokenSet | null = this._token_set; - if (!token_set && (token_set = await this._loadTokenSet())){ - this._token_set = token_set; + async #getTokenSet(): Promise { + let tokenSet: TokenSet | null = this.#tokenSet; + if (!tokenSet && (tokenSet = await this.#loadTokenSet())){ + this.#tokenSet = tokenSet; } - if (!token_set || !token_set.refresh_token) { - token_set = await this._authorize(); - } else if (!token_set.expires_at || token_set.expires_at < (Date.now() / 1000) + 10) { - token_set = await this._refresh(token_set.refresh_token); + if (!tokenSet || !tokenSet.refresh_token) { + tokenSet = await this.#authorize(); + } else if (!tokenSet.expires_at || tokenSet.expires_at < (Date.now() / 1000) + 10) { + tokenSet = await this.#refresh(tokenSet.refresh_token); } - if (this._token_set !== token_set) { - await this._storeTokenSet(token_set); + if (this.#tokenSet !== tokenSet) { + await this.#storeTokenSet(tokenSet); } - this._token_set = token_set; - return token_set; + this.#tokenSet = tokenSet; + return tokenSet; } - private async _getTokenSetQueued(): Promise { + async #getTokenSetQueued(): Promise { return new Promise((resolve, reject) => { - this._get_token_set_queue.push({ resolve, reject }); - if (this._get_token_set_queue.length === 1) { - this._getTokenSet() - .then((token_set) => { - this._get_token_set_queue.forEach(({ resolve }) => resolve(token_set)); - this._get_token_set_queue = []; + this.#getTokenSetQueue.push({ resolve, reject }); + if (this.#getTokenSetQueue.length === 1) { + this.#getTokenSet() + .then((tokenSet) => { + this.#getTokenSetQueue.forEach(({ resolve }) => resolve(tokenSet)); + this.#getTokenSetQueue = []; }) .catch((err) => { - this._get_token_set_queue.forEach(({ reject }) => reject(err)); - this._get_token_set_queue = []; + this.#getTokenSetQueue.forEach(({ reject }) => reject(err)); + this.#getTokenSetQueue = []; }); } }); } async requestResource(path: string, opts?: Parameters[2]): Promise { - const token_set = await this._getTokenSetQueued(); + const tokenSet = await this.#getTokenSetQueued(); const url = `${OnectaAPIBaseUrl.prod}${path}`; - const res = await this._client.requestResource(url, token_set, opts); + const res = await this.#client.requestResource(url, tokenSet, opts); if (res.body) { return JSON.parse(res.body.toString()); } diff --git a/src/onecta/oidc-utils.ts b/src/onecta/oidc-utils.ts index e310809..7c75a9e 100644 --- a/src/onecta/oidc-utils.ts +++ b/src/onecta/oidc-utils.ts @@ -1,21 +1,20 @@ - -import { Issuer } from 'openid-client'; +import { Issuer, TokenSet } from 'openid-client'; export enum OnectaOIDCScope { - basic='openid onecta:basic.integration', -}; + basic = 'openid onecta:basic.integration', +} export enum OnectaAPIBaseUrl { - prod='https://api.onecta.daikineurope.com', - mock='https://api.onecta.daikineurope.com/mock', -}; + prod = 'https://api.onecta.daikineurope.com', + mock = 'https://api.onecta.daikineurope.com/mock', +} export enum OnectaOIDCEndpoint { - authorization='https://idp.onecta.daikineurope.com/v1/oidc/authorize', - token='https://idp.onecta.daikineurope.com/v1/oidc/token', - revocation='https://idp.onecta.daikineurope.com/v1/oidc/revoke', - introspection='https://idp.onecta.daikineurope.com/v1/oidc/introspect', -}; + authorization = 'https://idp.onecta.daikineurope.com/v1/oidc/authorize', + token = 'https://idp.onecta.daikineurope.com/v1/oidc/token', + revocation = 'https://idp.onecta.daikineurope.com/v1/oidc/revoke', + introspection = 'https://idp.onecta.daikineurope.com/v1/oidc/introspect', +} export const onecta_oidc_issuer = new Issuer({ issuer: 'Daikin', @@ -32,17 +31,23 @@ export const onecta_oidc_auth_thank_you_html = `

Authorization complete

-

Thank you for authorizing daikin-onecta2mqtt to access your devices.

+

Thank you for authorizing daikin-controller-cloud to access your devices.

`; export interface OnectaClientConfig { - oidc_client_id: string; - oidc_client_secret: string; - oidc_callback_server_baseurl: string; - oidc_callback_server_port: number; - oidc_callback_server_addr: string; - oidc_authorization_timeout: number; - oidc_tokenset_file_path: string; + oidcClientId: string; + oidcClientSecret: string; + oidcCallbackServerExternalAddress?: string; + oidcCallbackServerBaseUrl?: string; + oidcCallbackServerPort: number; + oidcCallbackServerBindAddr: string; + oidcAuthorizationTimeoutS: number; + oidcTokenSetFilePath?: string; + certificatePathCert?: string; + certificatePathKey?: string; + onectaOidcAuthThankYouHtml?: string; + customOidcCodeReceiver?: (auth_url: string, state: string) => Promise; + tokenSet?: TokenSet; }