@abtnode/blocklet-services 1.16.47-beta-20250730-070104-87a128a5 → 1.16.47-beta-20250801-094129-1ba43aaa
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/api/index.js +16 -3
- package/api/libs/image.js +131 -77
- package/api/routes/blocklet.js +47 -32
- package/api/routes/csp-proxy.js +129 -14
- package/api/routes/user.js +6 -2
- package/api/services/auth/connect/verify-destroy.js +24 -2
- package/api/services/auth/session.js +6 -2
- package/api/services/image/index.js +6 -1
- package/dist/assets/AdapterDayjs-BN6xrcXw.js +3 -0
- package/dist/assets/{Google-kE2qbn1K.js → Google-BfPT9Xh-.js} +1 -1
- package/dist/assets/{access-control-FYs8JoGO.js → access-control-DkEAPrH2.js} +1 -1
- package/dist/assets/{actions-DvIZNF27.js → actions-CJMVMPKl.js} +1 -1
- package/dist/assets/{add-component-core-BB6rCC8n.js → add-component-core-B_I64xA8.js} +1 -1
- package/dist/assets/{add-resource-BlKrIrw0.js → add-resource-OtGLfsSC.js} +1 -1
- package/dist/assets/{addon-g3Lz4kfk.js → addon-ByRu0wmY.js} +1 -1
- package/dist/assets/{advanced-DyVpf_DB.js → advanced-BnXe4cqK.js} +1 -1
- package/dist/assets/aigne-CHLpC-y1.js +10 -0
- package/dist/assets/api-B6VBwg-Z.js +1 -0
- package/dist/assets/{appearance-D0WJqn3Y.js → appearance-DoL2p8uj.js} +1 -1
- package/dist/assets/ar-BmxvvhkF.js +7 -0
- package/dist/assets/{arrow-down.svg-CH8Xzna_.js → arrow-down.svg-CTyHGzaE.js} +1 -1
- package/dist/assets/{audit-logs-BLZ_FXya.js → audit-logs-CYCXwJKF.js} +1 -1
- package/dist/assets/authorize-CgriXRp-.js +1 -0
- package/dist/assets/{base-chart-Dhrxq8mT.js → base-chart-Bq5ougoU.js} +1 -1
- package/dist/assets/{base32-B9yl211y.js → base32-CiFWWnw3.js} +1 -1
- package/dist/assets/{branding-B_9Ik1Ul.js → branding-BHOpQPVg.js} +1 -1
- package/dist/assets/{branding-CbIhan7S.js → branding-BVhkSHKy.js} +2 -2
- package/dist/assets/{bundle-avatar-DzIGEDbC.js → bundle-avatar-EjC4ul9m.js} +1 -1
- package/dist/assets/button-DeYwq4G-.js +1 -0
- package/dist/assets/{click-to-copy-C43vd7DG.js → click-to-copy-BAVxsRT1.js} +1 -1
- package/dist/assets/{cloneDeep-DwV-xVQF.js → cloneDeep-Bp5oPxU3.js} +1 -1
- package/dist/assets/{collapse-B0wJmKRl.js → collapse-C3V-lD7M.js} +1 -1
- package/dist/assets/{complete-Cx5YFdNr.js → complete-E7j8kDa5.js} +1 -1
- package/dist/assets/{component-99-uAXc0.js → component-CDAFdcnx.js} +56 -72
- package/dist/assets/{config-Dr0r0k-g.js → config-B6qd-J_H.js} +1 -1
- package/dist/assets/{config-B4lheVsX.js → config-Dlwx7WyO.js} +1 -1
- package/dist/assets/config-FumbXhpa.js +263 -0
- package/dist/assets/{config-navigation-ToPhdOjg.js → config-navigation-Cr1v50S5.js} +3 -3
- package/dist/assets/{config-space-CT21iR_2.js → config-space-OVPR-H3Q.js} +1 -1
- package/dist/assets/{confirm-5e46DDlz.js → confirm-DSkTNclg.js} +1 -1
- package/dist/assets/{connect-hooMfIaO.js → connect-BKQ09vcI.js} +1 -1
- package/dist/assets/{connect-BHDDk939.js → connect-VkcmO-ox.js} +1 -1
- package/dist/assets/{connect-to-DfxSuVNx.js → connect-to-D4y7gfX6.js} +1 -1
- package/dist/assets/{content-layout--aahhgT-.js → content-layout-CT98QJfh.js} +1 -1
- package/dist/assets/{createClass-BuE_i6UP.js → createClass-BxvbX2Fy.js} +1 -1
- package/dist/assets/dashboard-CCmzTOat.js +106 -0
- package/dist/assets/de-7aojY3wG.js +7 -0
- package/dist/assets/{delete-confirm-Bim07gVu.js → delete-confirm-CZyKrY8X.js} +1 -1
- package/dist/assets/{did-address-DrmEnz9K.js → did-address-D1KZLP2_.js} +1 -1
- package/dist/assets/{domain-D_RRXHNv.js → domain-BLaBUwtN.js} +1 -1
- package/dist/assets/{domain-action-card-BTosQrln.js → domain-action-card-CtdHon3A.js} +1 -1
- package/dist/assets/domains-C4bCGzUa.js +1 -0
- package/dist/assets/{dot-BaR-8Dj-.js → dot-BD1gx_6x.js} +1 -1
- package/dist/assets/{email-BCDbiSlJ.js → email-DprB1ao7.js} +2 -2
- package/dist/assets/{empty-spinner-D6ajz4La.js → empty-spinner-BMuZ6jkU.js} +1 -1
- package/dist/assets/engine-BYtxLgE-.js +1 -0
- package/dist/assets/es-CEkR13ZB.js +9 -0
- package/dist/assets/{exchange-passport-DIp9EJVY.js → exchange-passport-Ba2lo0my.js} +1 -1
- package/dist/assets/{form-BVnHvgNl.js → form-bIhrdHBk.js} +1 -1
- package/dist/assets/{form-text-input-x3g3qd72.js → form-text-input-DpFFySn2.js} +1 -1
- package/dist/assets/{form-wrapper-DimyA4Kf.js → form-wrapper-CpayHdi1.js} +1 -1
- package/dist/assets/fr-Blm8Lvo8.js +7 -0
- package/dist/assets/{fuel-Fh-ifxfq.js → fuel-DrfkIg-0.js} +1 -1
- package/dist/assets/{gen-access-key-BZufQ91-.js → gen-access-key-CM92my-2.js} +1 -1
- package/dist/assets/{gen-simple-access-key-DlPE_0y2.js → gen-simple-access-key-BmVdze52.js} +1 -1
- package/dist/assets/get-safe-url-CzyoiDvV.js +1 -0
- package/dist/assets/hi-D-gLlKa4.js +5 -0
- package/dist/assets/{home-B6em2u64.js → home-vNW_uCnj.js} +1 -1
- package/dist/assets/id-Dp1w98Mc.js +7 -0
- package/dist/assets/{iframe-Dl-5xRyx.js → iframe-H_M17Rw0.js} +1 -1
- package/dist/assets/{index-BituGg_i.js → index-5QMAQjkj.js} +1 -1
- package/dist/assets/{index-DIeXMLLs.js → index-9LrJ71wP.js} +1 -1
- package/dist/assets/index-B-nFCTvl.js +1 -0
- package/dist/assets/{index-B_JPa3k-.js → index-B7WwBeaA.js} +1 -1
- package/dist/assets/{index-Eagwj9HU.js → index-BCK2cqC6.js} +1 -1
- package/dist/assets/{index-D6ZAoKh5.js → index-BRPXjkAq.js} +1 -1
- package/dist/assets/{index-DoJKIwOt.js → index-BaofAoCa.js} +1 -1
- package/dist/assets/{index-DLDRrQJH.js → index-BeaB_cKh.js} +1 -1
- package/dist/assets/{index-DEDOywis.js → index-Bfm7dO_D.js} +1 -1
- package/dist/assets/{index-D6YhMyoB.js → index-Bm3aC1-S.js} +63 -63
- package/dist/assets/{index-DscHS5BG.js → index-Bp3op3AB.js} +1 -1
- package/dist/assets/{index-YhHgNT9f.js → index-BuOGtUh1.js} +1 -1
- package/dist/assets/index-ByXiBZvQ.js +1 -0
- package/dist/assets/{index-DZFL6rrK.js → index-CN_vv3DD.js} +1 -1
- package/dist/assets/{index-B88jr6sm.js → index-CUl7rwhw.js} +1 -1
- package/dist/assets/{index-DraljbB7.js → index-CUtbL7Pc.js} +9 -9
- package/dist/assets/{index-Cq-4YOTs.js → index-Co2hRThu.js} +1 -1
- package/dist/assets/{index-DmgU5YEy.js → index-CzWvmP1s.js} +1 -1
- package/dist/assets/{index-ckUPtmoF.js → index-D7O86bEq.js} +2 -2
- package/dist/assets/{index-DTuL5S3g.js → index-DKqTApAY.js} +1 -1
- package/dist/assets/{index-CkuzlAat.js → index-DZqnYK-R.js} +1 -1
- package/dist/assets/{index-1-P2gWGZ.js → index-DefvhIEx.js} +1 -1
- package/dist/assets/{index-W7Vsgxmq.js → index-DhpSUbJa.js} +19 -19
- package/dist/assets/{index-CC1_0xzr.js → index-Du6WLi38.js} +4 -4
- package/dist/assets/{index-B6RoOcNF.js → index-DuiAdMaP.js} +1 -1
- package/dist/assets/{index-h4nZrNlX.js → index-IQj51U6i.js} +1 -1
- package/dist/assets/{index-BFAR00t8.js → index-Vc0vthSd.js} +1 -1
- package/dist/assets/index-ZAHGzjDt.js +124 -0
- package/dist/assets/{index-CiFQnBvO.js → index-aJi9Bf-D.js} +1 -1
- package/dist/assets/{index-Ca2q4nT6.js → index-j-o7Fhl2.js} +1 -1
- package/dist/assets/{index-DiW2Y89J.js → index-six7TSvm.js} +10 -10
- package/dist/assets/{index-D8gGAVdB.js → index-vq54lJAL.js} +1 -1
- package/dist/assets/{invitation-ygFxiD0p.js → invitation-BsCAwa_S.js} +6 -6
- package/dist/assets/invite-BgoFaNhy.js +1 -0
- package/dist/assets/{isURL-CTW__dzu.js → isURL-BpO9T7jO.js} +1 -1
- package/dist/assets/issue-passport-BukWrnf4.js +1 -0
- package/dist/assets/{item-BpwJ2QYy.js → item-lF1pMg0y.js} +1 -1
- package/dist/assets/ja-DYfvAc_h.js +9 -0
- package/dist/assets/ko-DeeF0knw.js +9 -0
- package/dist/assets/{landing-page-Cd61UT7Z.js → landing-page-Bje6A6LW.js} +1 -1
- package/dist/assets/{launch-result-message-C7170Bvt.js → launch-result-message-DDqxU5ge.js} +1 -1
- package/dist/assets/{layout-Bf7XRYSR.js → layout-ffvsYKlt.js} +1 -1
- package/dist/assets/{list-BHhWPWs4.js → list-D1xRxqmd.js} +2 -2
- package/dist/assets/{list-A-6ZKqgn.js → list-d3KGpyvC.js} +1 -1
- package/dist/assets/{list-header-X0CL-_hO.js → list-header-BDPLDqO-.js} +1 -1
- package/dist/assets/localization-D8EUC9hz.js +1 -0
- package/dist/assets/{log-ryKQFCdM.js → log-DaUV-KQg.js} +1 -1
- package/dist/assets/logger-BkCCiTnd.js +1 -0
- package/dist/assets/{login-CYXIFDX8.js → login-C_FzEtDr.js} +1 -1
- package/dist/assets/{login-oauth-callback-DAKrg2Ne.js → login-oauth-callback-Br-fBLIM.js} +1 -1
- package/dist/assets/{logo-uploader-DDncXmM_.js → logo-uploader-DpVGDA9S.js} +2 -2
- package/dist/assets/{lost-passport-DEoDgh2I.js → lost-passport-7dfs_oPd.js} +1 -1
- package/dist/assets/{observability-DVi-zwiF.js → observability-C5Y6fafw.js} +1 -1
- package/dist/assets/{omit-8QDODHip.js → omit-DDVAqzZq.js} +1 -1
- package/dist/assets/{open-window-eYnS4Amh.js → open-window-DZM6BqqN.js} +1 -1
- package/dist/assets/{over-due-invoice-payment-CnaSUbBp.js → over-due-invoice-payment-UPFNS992.js} +1 -1
- package/dist/assets/{overview-BbhCPjub.js → overview-D-k9QnCr.js} +1 -1
- package/dist/assets/{page-header-1pVKKoSa.js → page-header-B9Q5ZOnJ.js} +1 -1
- package/dist/assets/{passport-item-CPGYYOeu.js → passport-item-tw8M12dr.js} +1 -1
- package/dist/assets/{permission-B0EjzZOA.js → permission-tgoZwbT0.js} +1 -1
- package/dist/assets/{playground-CveQRqPB.js → playground-DUuQxqZF.js} +1 -1
- package/dist/assets/preferences-BzGoZ4H7.js +1 -0
- package/dist/assets/profile-embed-UV7c2kes.js +1 -0
- package/dist/assets/pt-B2XwgtkL.js +5 -0
- package/dist/assets/publish-resource-DzuGaf5F.js +1 -0
- package/dist/assets/{react-beautiful-dnd.esm-CdYP6hjZ.js → react-beautiful-dnd.esm-Bn1B6RSE.js} +1 -1
- package/dist/assets/{react-stripe.esm-3alxb6kT.js → react-stripe.esm-DlCFj5cL.js} +1 -1
- package/dist/assets/{required-qTx6pgJa.js → required-mwCee7dE.js} +1 -1
- package/dist/assets/ru-BQnkoIIE.js +5 -0
- package/dist/assets/runtime-tLrFToGI.js +1 -0
- package/dist/assets/{sanitize-BSjwjSMl.js → sanitize-d0BswqQy.js} +1 -1
- package/dist/assets/sdk-Dli060me.js +1 -0
- package/dist/assets/{section-DLNNkzJC.js → section-DkbgrVvQ.js} +1 -1
- package/dist/assets/{security-DzpCKO8Z.js → security-Chcv4uGK.js} +3 -3
- package/dist/assets/{session-fyUi2EvV.js → session-XtuHaexA.js} +1 -1
- package/dist/assets/setup-DT83Wy3z.js +30 -0
- package/dist/assets/{shorten-label-C8Cay9FW.js → shorten-label-CzJmma8P.js} +1 -1
- package/dist/assets/{simple-select-DCCkM3zE.js → simple-select-WHx-v2hU.js} +1 -1
- package/dist/assets/{spaces-Ci_JwPPC.js → spaces-Bz3V0eYU.js} +1 -1
- package/dist/assets/{start-CvpuRdlI.js → start-Cvh2uD-h.js} +1 -1
- package/dist/assets/{starting-progress-4SearER_.js → starting-progress-DGPdZeyx.js} +1 -1
- package/dist/assets/{status-BYn5dcxB.js → status-DRg5cWiY.js} +1 -1
- package/dist/assets/{step-actions-vNi6kxQk.js → step-actions-DreWQ64q.js} +1 -1
- package/dist/assets/{studio-CU2xUA6B.js → studio-qLDw0ukd.js} +1 -1
- package/dist/assets/{switch-control-Cd3JbWWV.js → switch-control-CjKF-gAU.js} +1 -1
- package/dist/assets/{table-tips-3_n5uR-X.js → table-tips-Bb8itsxN.js} +1 -1
- package/dist/assets/{team-Bxi69lmr.js → team-CDSer28u.js} +5 -5
- package/dist/assets/th-x7kvDKZJ.js +5 -0
- package/dist/assets/{traffic-5kSM9zFo.js → traffic-CUT8PqO4.js} +1 -1
- package/dist/assets/{transfer-1mPm3DHh.js → transfer-YfQr5dnn.js} +1 -1
- package/dist/assets/{unsubscribe-BHRGSL9f.js → unsubscribe-Bzl4TSbX.js} +1 -1
- package/dist/assets/{use-mobile-B9FM-UBY.js → use-mobile-D0T_V7RQ.js} +1 -1
- package/dist/assets/{use-mobile-BC9t6zmM.js → use-mobile-IWOBvgsY.js} +1 -1
- package/dist/assets/{use-promise-window-open-DzOXAig9.js → use-promise-window-open-D_ahP8Mz.js} +1 -1
- package/dist/assets/{use-server-logo-xLbI9v7G.js → use-server-logo-CUO3ZK3W.js} +1 -1
- package/dist/assets/{use-window-close-CleYtwbz.js → use-window-close-Cp670vvd.js} +1 -1
- package/dist/assets/{useAsync-BdgUavMa.js → useAsync-CQhKDImp.js} +1 -1
- package/dist/assets/{useAsync-DgPr_To1.js → useAsync-VHQLt8EO.js} +1 -1
- package/dist/assets/{useLocalStorage-Dgwq5dGM.js → useLocalStorage-DVoRz1Om.js} +1 -1
- package/dist/assets/user-center-C1W41jVp.js +126 -0
- package/dist/assets/{user-sessions-CFuRw6Kk.js → user-sessions-DeUIUEmh.js} +1 -1
- package/dist/assets/{util-CxrGdmDd.js → util-C7XciGgo.js} +1 -1
- package/dist/assets/{util-DhFzZqQY.js → util-PB33gwP1.js} +1 -1
- package/dist/assets/{vendor-arcblock-BnbwP1kf.js → vendor-arcblock-t44X0l6F.js} +1 -1
- package/dist/assets/{vendor-hooks-DTeIFVkk.js → vendor-hooks-B2bT6Nqf.js} +1 -1
- package/dist/assets/{vendor-mui-core-CcrDM-5l.js → vendor-mui-core-Bw-IjQ9m.js} +1 -1
- package/dist/assets/{vendor-mui-x-BVRWKjdg.js → vendor-mui-x-CuLvhfRb.js} +14 -14
- package/dist/assets/{vendor-utils-BOkrEpKO.js → vendor-utils-B4mliPf7.js} +1 -1
- package/dist/assets/{vendor-ux-CUBj5GXM.js → vendor-ux-DcEgaaV8.js} +3 -3
- package/dist/assets/vi-CT5pkl6Q.js +5 -0
- package/dist/assets/{wait-connect-Cht9WKDH.js → wait-connect-DrZC6ybw.js} +1 -1
- package/dist/assets/wrap-locale-zQpfFxI2.js +1 -0
- package/dist/assets/zh-ATukbAXz.js +10 -0
- package/dist/assets/{zh-IccOJN0P.js → zh-Cc55bM2s.js} +1 -1
- package/dist/assets/zh-tw-dw7nLLI_.js +9 -0
- package/dist/index.html +6 -6
- package/dist/service-worker.js +1 -1
- package/package.json +35 -34
- package/dist/assets/AdapterDayjs-BEkLDdDJ.js +0 -3
- package/dist/assets/aigne-B2kmHT_s.js +0 -10
- package/dist/assets/api-Rnh95Dt4.js +0 -1
- package/dist/assets/ar-7eC8LKj2.js +0 -7
- package/dist/assets/authorize-DqbV7Yj5.js +0 -1
- package/dist/assets/button-CPgviFPs.js +0 -1
- package/dist/assets/config-yxcrU66X.js +0 -26
- package/dist/assets/dashboard-DaKd1daE.js +0 -106
- package/dist/assets/de-DgwOa_mf.js +0 -7
- package/dist/assets/domains-BV1O_rDN.js +0 -1
- package/dist/assets/engine-BVHpout7.js +0 -1
- package/dist/assets/es-B_gBSKp3.js +0 -9
- package/dist/assets/fr-Bm0AhLrO.js +0 -7
- package/dist/assets/get-safe-url-D9ZX2qFm.js +0 -1
- package/dist/assets/hi-DO31L4at.js +0 -5
- package/dist/assets/id-BYCjnoK8.js +0 -7
- package/dist/assets/index-467tNKTx.js +0 -124
- package/dist/assets/index-7fdKVdEw.js +0 -1
- package/dist/assets/index-pvfQRiIz.js +0 -1
- package/dist/assets/invite-DMpU1WKR.js +0 -1
- package/dist/assets/issue-passport-CH_GIDha.js +0 -1
- package/dist/assets/ja-Ck2EVKeO.js +0 -9
- package/dist/assets/ko-Nk_6p6Jl.js +0 -9
- package/dist/assets/localization-D1S2pks-.js +0 -1
- package/dist/assets/logger-MoQC3rmC.js +0 -1
- package/dist/assets/preferences-CoP65AxI.js +0 -1
- package/dist/assets/profile-embed-CdAkuRfH.js +0 -1
- package/dist/assets/pt-C-JzkWqR.js +0 -5
- package/dist/assets/publish-resource-O6nixlJ9.js +0 -1
- package/dist/assets/ru-DR6is0tr.js +0 -5
- package/dist/assets/runtime-lAc0mbn0.js +0 -1
- package/dist/assets/sdk-CiRRFKs0.js +0 -1
- package/dist/assets/setup-BtNJlN-g.js +0 -30
- package/dist/assets/th-Cn3755Vi.js +0 -5
- package/dist/assets/user-center-D7U8E_9U.js +0 -126
- package/dist/assets/vi-7MIaoDt9.js +0 -5
- package/dist/assets/wrap-locale-CnZSJgmv.js +0 -1
- package/dist/assets/zh-DswrR4Lx.js +0 -10
- package/dist/assets/zh-tw-De_IfliP.js +0 -9
package/api/index.js
CHANGED
|
@@ -13,7 +13,8 @@ const httpProxy = require('@arcblock/http-proxy');
|
|
|
13
13
|
const { minimatch } = require('minimatch');
|
|
14
14
|
const helmet = require('helmet');
|
|
15
15
|
const isUrl = require('is-url');
|
|
16
|
-
const { WELLKNOWN_SERVICE_PATH_PREFIX, EVENTS } = require('@abtnode/constant');
|
|
16
|
+
const { WELLKNOWN_SERVICE_PATH_PREFIX, EVENTS, WELLKNOWN_BLOCKLET_ADMIN_PATH } = require('@abtnode/constant');
|
|
17
|
+
const getBlockletComingSoonTemplate = require('@abtnode/router-templates/lib/blocklet-coming-soon');
|
|
17
18
|
const {
|
|
18
19
|
BlockletEvents,
|
|
19
20
|
BlockletInternalEvents,
|
|
@@ -147,11 +148,23 @@ module.exports = function createServer(node, serverOptions = {}) {
|
|
|
147
148
|
});
|
|
148
149
|
|
|
149
150
|
proxy.safeWeb = (req, res, opts = {}) => {
|
|
150
|
-
proxy.web(req, res, opts, (error) => {
|
|
151
|
+
proxy.web(req, res, opts, async (error) => {
|
|
151
152
|
if (error) {
|
|
152
153
|
logger.error('http proxy error', { error, raw: req.originalUrl, opts });
|
|
153
154
|
if (!res.headersSent) {
|
|
154
|
-
|
|
155
|
+
try {
|
|
156
|
+
const [blocklet, nodeInfo] = await Promise.all([req.getBlocklet(), req.getNodeInfo()]);
|
|
157
|
+
res
|
|
158
|
+
.status(502)
|
|
159
|
+
.send(
|
|
160
|
+
blocklet && nodeInfo
|
|
161
|
+
? getBlockletComingSoonTemplate(blocklet, nodeInfo, WELLKNOWN_BLOCKLET_ADMIN_PATH)
|
|
162
|
+
: `Can not proxy to upstream target: ${opts.target}`
|
|
163
|
+
);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
logger.error('Failed to get blocklet or node info', { err });
|
|
166
|
+
res.status(502).send(`Can not proxy to upstream target: ${opts.target}`);
|
|
167
|
+
}
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
});
|
package/api/libs/image.js
CHANGED
|
@@ -108,7 +108,8 @@ const getCacheFilePath = (dataDir, fileName) => {
|
|
|
108
108
|
return result;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
// NOTICE: 如果传入了 fileName,则优先使用 fileName 的 extension,否则使用 req.path 的 extension
|
|
112
|
+
const isImageRequest = (req, fileName) => {
|
|
112
113
|
if (req.method !== 'GET') {
|
|
113
114
|
return false;
|
|
114
115
|
}
|
|
@@ -122,7 +123,8 @@ const isImageRequest = (req) => {
|
|
|
122
123
|
if (!req.query.imageFilter) {
|
|
123
124
|
return false;
|
|
124
125
|
}
|
|
125
|
-
const extension = toLower(path.extname(req.path).slice(1));
|
|
126
|
+
const extension = toLower(path.extname(fileName || req.path).slice(1));
|
|
127
|
+
|
|
126
128
|
if (extension && !FORMATS.includes(EXTENSIONS[extension])) {
|
|
127
129
|
return false;
|
|
128
130
|
}
|
|
@@ -138,95 +140,136 @@ const isImageRequest = (req) => {
|
|
|
138
140
|
|
|
139
141
|
const getImageContentType = (extension) => (extension === 'svg' ? 'image/svg+xml' : `image/${extension}`);
|
|
140
142
|
|
|
141
|
-
const processImage = (
|
|
143
|
+
const processImage = (srcStream, { extension, destPath, srcPath }, filterParams = {}) => {
|
|
142
144
|
return new Promise((resolve, reject) => {
|
|
143
145
|
// output stream
|
|
144
|
-
const out = fs.createWriteStream(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
const out = fs.createWriteStream(destPath);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// 这里是双重保障, /.blocklet/service/blocklet/logo 已经在 isImageRequest 中可以过滤掉了
|
|
150
|
+
// 此处只是为了增加健壮性,一般情况下不会到这里
|
|
151
|
+
if (extension && !FORMATS.includes(EXTENSIONS[extension])) {
|
|
152
|
+
// 如果是不支持的 extension,则将源文件直接写入目标地址
|
|
153
|
+
srcStream.pipe(out);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
out.on('close', () => {
|
|
158
|
+
resolve(destPath);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
out.on('error', (err) => {
|
|
162
|
+
reject(err);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const {
|
|
166
|
+
imageFilter,
|
|
167
|
+
w: width,
|
|
168
|
+
h: height,
|
|
169
|
+
t: top,
|
|
170
|
+
l: left,
|
|
171
|
+
q: quality,
|
|
172
|
+
f: format,
|
|
173
|
+
m: mode,
|
|
174
|
+
r: rotate,
|
|
175
|
+
p: progressive,
|
|
176
|
+
g: greyscale,
|
|
177
|
+
b: blur,
|
|
178
|
+
a: transparency,
|
|
179
|
+
n: negative,
|
|
180
|
+
s: sharpen,
|
|
181
|
+
} = filterParams;
|
|
182
|
+
|
|
183
|
+
const dimensions = { top, left };
|
|
184
|
+
if (width) {
|
|
185
|
+
dimensions.width = width;
|
|
186
|
+
}
|
|
187
|
+
if (height) {
|
|
188
|
+
dimensions.height = height;
|
|
189
|
+
}
|
|
152
190
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
} = params;
|
|
170
|
-
|
|
171
|
-
const dimensions = { top, left };
|
|
172
|
-
if (width) {
|
|
173
|
-
dimensions.width = width;
|
|
174
|
-
}
|
|
175
|
-
if (height) {
|
|
176
|
-
dimensions.height = height;
|
|
177
|
-
}
|
|
191
|
+
const pipeline = sharp({ animated: true, limitInputPixels: 0 }).timeout({ seconds: 60 });
|
|
192
|
+
if (rotate) {
|
|
193
|
+
pipeline.rotate(rotate);
|
|
194
|
+
}
|
|
195
|
+
if (imageFilter === 'resize') {
|
|
196
|
+
if (!dimensions.width && !dimensions.height) {
|
|
197
|
+
reject(new CustomError(400, 'At least one of `w` or `h` must be provided to resize'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (dimensions.width || dimensions.height) {
|
|
202
|
+
pipeline.resize({ ...dimensions, fit: mode, withoutEnlargement: true });
|
|
203
|
+
}
|
|
204
|
+
if (imageFilter === 'crop') {
|
|
205
|
+
pipeline.extract(dimensions);
|
|
206
|
+
}
|
|
178
207
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
if (sharpen) {
|
|
209
|
+
pipeline.sharpen(sharpen);
|
|
210
|
+
}
|
|
211
|
+
if (blur) {
|
|
212
|
+
pipeline.blur(blur);
|
|
213
|
+
}
|
|
214
|
+
if (greyscale) {
|
|
215
|
+
pipeline.greyscale();
|
|
216
|
+
}
|
|
217
|
+
if (transparency) {
|
|
218
|
+
pipeline.ensureAlpha();
|
|
219
|
+
} else {
|
|
220
|
+
pipeline.removeAlpha();
|
|
221
|
+
}
|
|
222
|
+
if (negative) {
|
|
223
|
+
pipeline.negate();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const processFn = pipeline[format || EXTENSIONS[extension]];
|
|
227
|
+
// 如果 sharp 实例中不包含目标 extension 的转换方法,则将源文件直接写入目标地址
|
|
228
|
+
if (!processFn || processFn instanceof Function === false) {
|
|
229
|
+
srcStream.pipe(out);
|
|
186
230
|
return;
|
|
187
231
|
}
|
|
188
|
-
}
|
|
189
|
-
if (dimensions.width || dimensions.height) {
|
|
190
|
-
pipeline.resize({ ...dimensions, fit: mode, withoutEnlargement: true });
|
|
191
|
-
}
|
|
192
|
-
if (imageFilter === 'crop') {
|
|
193
|
-
pipeline.extract(dimensions);
|
|
194
|
-
}
|
|
195
232
|
|
|
196
|
-
|
|
197
|
-
pipeline
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
pipeline
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
233
|
+
// HACK: 这里不能使用 processFn 来替代,会导致图片处理出错(猜测是 this 指针的问题)
|
|
234
|
+
pipeline[format || EXTENSIONS[extension]]({ quality, progressive: !!progressive, dither: 0, force: true });
|
|
235
|
+
|
|
236
|
+
pipeline.on('error', (err) => {
|
|
237
|
+
reject(err);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// run the pipeline
|
|
241
|
+
srcStream.pipe(pipeline).pipe(out);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
logger.error('image filter failed', {
|
|
244
|
+
error: err,
|
|
245
|
+
filterParams,
|
|
246
|
+
srcPath,
|
|
247
|
+
destPath,
|
|
248
|
+
extension,
|
|
249
|
+
});
|
|
250
|
+
srcStream.pipe(out);
|
|
209
251
|
}
|
|
210
|
-
if (negative) {
|
|
211
|
-
pipeline.negate();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
pipeline[format || EXTENSIONS[extension]]({ quality, progressive: !!progressive, dither: 0, force: true });
|
|
215
|
-
|
|
216
|
-
pipeline.on('error', (err) => {
|
|
217
|
-
reject(err);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// run the pipeline
|
|
221
|
-
src.pipe(pipeline).pipe(out);
|
|
222
252
|
}).catch((err) => {
|
|
223
|
-
rmSync(
|
|
253
|
+
rmSync(destPath, { force: true });
|
|
224
254
|
throw err;
|
|
225
255
|
});
|
|
226
256
|
};
|
|
227
257
|
|
|
228
258
|
const tasks = {};
|
|
229
|
-
const processAndRespond = (
|
|
259
|
+
const processAndRespond = (
|
|
260
|
+
req,
|
|
261
|
+
res,
|
|
262
|
+
{
|
|
263
|
+
srcPath,
|
|
264
|
+
cacheDir,
|
|
265
|
+
getSrc,
|
|
266
|
+
extension: ext,
|
|
267
|
+
sendOptions = {
|
|
268
|
+
maxAge: '356d',
|
|
269
|
+
immutable: true,
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
) => {
|
|
230
273
|
if (fs.existsSync(cacheDir) === false) {
|
|
231
274
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
232
275
|
}
|
|
@@ -255,6 +298,7 @@ const processAndRespond = (req, res, cacheDir, getSrc, ext, sendOptions = { maxA
|
|
|
255
298
|
|
|
256
299
|
const cacheKey = md5(stringify({ target: req.target, path: req.originalUrl, params }));
|
|
257
300
|
const destPath = getCacheFilePath(cacheDir, `${cacheKey}.${params.f || extension}`);
|
|
301
|
+
|
|
258
302
|
if (fs.existsSync(destPath)) {
|
|
259
303
|
res.header('Content-Type', getImageContentType(params.f || extension));
|
|
260
304
|
res.sendFile(destPath, sendOptions);
|
|
@@ -263,7 +307,17 @@ const processAndRespond = (req, res, cacheDir, getSrc, ext, sendOptions = { maxA
|
|
|
263
307
|
|
|
264
308
|
// do the convert
|
|
265
309
|
tasks[cacheKey] ??= getSrc(req)
|
|
266
|
-
.then(([src, _extension]) =>
|
|
310
|
+
.then(([src, _extension]) =>
|
|
311
|
+
processImage(
|
|
312
|
+
src,
|
|
313
|
+
{
|
|
314
|
+
extension: toLower(_extension),
|
|
315
|
+
destPath,
|
|
316
|
+
srcPath,
|
|
317
|
+
},
|
|
318
|
+
params
|
|
319
|
+
)
|
|
320
|
+
)
|
|
267
321
|
.finally(() => {
|
|
268
322
|
setTimeout(() => {
|
|
269
323
|
delete tasks[cacheKey];
|
package/api/routes/blocklet.js
CHANGED
|
@@ -74,17 +74,16 @@ module.exports = {
|
|
|
74
74
|
|
|
75
75
|
const attachSendLogoFn = (req, res, next) => {
|
|
76
76
|
res.sendLogoFile = (fileName, options) => {
|
|
77
|
-
if (isImageRequest(req)) {
|
|
77
|
+
if (isImageRequest(req, fileName)) {
|
|
78
78
|
logger.info('send logo with image service', { fileName, options });
|
|
79
79
|
const appDir = path.join(req.blocklet.env.cacheDir, '.services', 'image-filter');
|
|
80
|
-
processAndRespond(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
80
|
+
processAndRespond(req, res, {
|
|
81
|
+
srcPath: fileName,
|
|
82
|
+
cacheDir: appDir,
|
|
83
|
+
getSrc: () => Promise.resolve([fs.createReadStream(fileName), path.extname(fileName).slice(1)]),
|
|
84
|
+
extension: path.extname(fileName).slice(1),
|
|
85
|
+
sendOptions: req.sendOptions,
|
|
86
|
+
});
|
|
88
87
|
} else {
|
|
89
88
|
logger.info('send logo with file', { fileName, options });
|
|
90
89
|
res.sendFile(fileName, options);
|
|
@@ -165,20 +164,24 @@ module.exports = {
|
|
|
165
164
|
}
|
|
166
165
|
}
|
|
167
166
|
|
|
168
|
-
const
|
|
169
|
-
if (!fs.existsSync(
|
|
167
|
+
const avatarFilePath = getAvatarFile(dataDir, fileName);
|
|
168
|
+
if (!fs.existsSync(avatarFilePath)) {
|
|
170
169
|
res.status(404).send('Avatar Not Found');
|
|
171
170
|
return;
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
if (isImageAccepted(req) && isImageRequest(req)) {
|
|
175
174
|
const appDir = path.join(cacheDir, '.services', 'image-filter');
|
|
176
|
-
processAndRespond(req, res,
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
processAndRespond(req, res, {
|
|
176
|
+
srcPath: avatarFilePath,
|
|
177
|
+
cacheDir: appDir,
|
|
178
|
+
getSrc: () => {
|
|
179
|
+
stream = fs.createReadStream(avatarFilePath);
|
|
180
|
+
return Promise.resolve([stream, path.extname(avatarFilePath).slice(1)]);
|
|
181
|
+
},
|
|
179
182
|
});
|
|
180
183
|
} else {
|
|
181
|
-
res.sendFile(
|
|
184
|
+
res.sendFile(avatarFilePath, { maxAge: '365d', immutable: true });
|
|
182
185
|
}
|
|
183
186
|
} catch (err) {
|
|
184
187
|
stream?.destroy();
|
|
@@ -561,7 +564,7 @@ module.exports = {
|
|
|
561
564
|
info.ogImageHash = encodeURIComponent(ogImage.split('/').slice(-1)[0].slice(0, 7));
|
|
562
565
|
}
|
|
563
566
|
|
|
564
|
-
const
|
|
567
|
+
const sourceFilePath = await getOgImage({
|
|
565
568
|
input: req.query,
|
|
566
569
|
info,
|
|
567
570
|
format,
|
|
@@ -570,12 +573,16 @@ module.exports = {
|
|
|
570
573
|
});
|
|
571
574
|
if (format === 'png' && isImageAccepted(req) && isImageRequest(req)) {
|
|
572
575
|
const appDir = getAppImageCacheDir(blocklet.env.cacheDir);
|
|
573
|
-
processAndRespond(req, res,
|
|
574
|
-
|
|
575
|
-
|
|
576
|
+
processAndRespond(req, res, {
|
|
577
|
+
srcPath: sourceFilePath,
|
|
578
|
+
cacheDir: appDir,
|
|
579
|
+
getSrc: () => {
|
|
580
|
+
stream = fs.createReadStream(sourceFilePath);
|
|
581
|
+
return Promise.resolve([stream, path.extname(sourceFilePath).slice(1)]);
|
|
582
|
+
},
|
|
576
583
|
});
|
|
577
584
|
} else {
|
|
578
|
-
res.sendFile(
|
|
585
|
+
res.sendFile(sourceFilePath, cache ? { maxAge: '365d', immutable: true } : { maxAge: 0 });
|
|
579
586
|
}
|
|
580
587
|
} catch (err) {
|
|
581
588
|
stream?.destroy();
|
|
@@ -591,17 +598,21 @@ module.exports = {
|
|
|
591
598
|
const blocklet = await req.getBlocklet();
|
|
592
599
|
const appSplash = get(blocklet, `environmentObj.BLOCKLET_APP_SPLASH_${type.toUpperCase()}`);
|
|
593
600
|
if (appSplash) {
|
|
594
|
-
const
|
|
595
|
-
if (fs.existsSync(
|
|
601
|
+
const splashFilePath = path.join(get(blocklet, 'env.dataDir'), appSplash);
|
|
602
|
+
if (fs.existsSync(splashFilePath)) {
|
|
596
603
|
if (isImageRequest(req)) {
|
|
597
604
|
const appDir = getAppImageCacheDir(blocklet.env.cacheDir);
|
|
598
|
-
processAndRespond(req, res,
|
|
599
|
-
|
|
600
|
-
|
|
605
|
+
processAndRespond(req, res, {
|
|
606
|
+
srcPath: splashFilePath,
|
|
607
|
+
cacheDir: appDir,
|
|
608
|
+
getSrc: () => {
|
|
609
|
+
stream = fs.createReadStream(splashFilePath);
|
|
610
|
+
return Promise.resolve([stream, path.extname(splashFilePath).slice(1)]);
|
|
611
|
+
},
|
|
601
612
|
});
|
|
602
613
|
} else {
|
|
603
614
|
const cache = req.query.nocache !== '1';
|
|
604
|
-
res.sendFile(
|
|
615
|
+
res.sendFile(splashFilePath, cache ? { maxAge: '365d', immutable: true } : { maxAge: 0 });
|
|
605
616
|
}
|
|
606
617
|
} else {
|
|
607
618
|
res.sendFile(`/images/splash-${type}.png`, { root: staticDir, maxAge: '1h' });
|
|
@@ -622,17 +633,21 @@ module.exports = {
|
|
|
622
633
|
const blocklet = await req.getBlocklet();
|
|
623
634
|
const appOgImage = get(blocklet, 'environmentObj.BLOCKLET_APP_OG_IMAGE');
|
|
624
635
|
if (appOgImage) {
|
|
625
|
-
const
|
|
626
|
-
if (fs.existsSync(
|
|
636
|
+
const ogImageFilePath = path.join(get(blocklet, 'env.dataDir'), appOgImage);
|
|
637
|
+
if (fs.existsSync(ogImageFilePath)) {
|
|
627
638
|
if (isImageRequest(req)) {
|
|
628
639
|
const appDir = getAppImageCacheDir(blocklet.env.cacheDir);
|
|
629
|
-
processAndRespond(req, res,
|
|
630
|
-
|
|
631
|
-
|
|
640
|
+
processAndRespond(req, res, {
|
|
641
|
+
srcPath: ogImageFilePath,
|
|
642
|
+
cacheDir: appDir,
|
|
643
|
+
getSrc: () => {
|
|
644
|
+
stream = fs.createReadStream(ogImageFilePath);
|
|
645
|
+
return Promise.resolve([stream, path.extname(ogImageFilePath).slice(1)]);
|
|
646
|
+
},
|
|
632
647
|
});
|
|
633
648
|
} else {
|
|
634
649
|
const cache = req.query.nocache !== '1';
|
|
635
|
-
res.sendFile(
|
|
650
|
+
res.sendFile(ogImageFilePath, cache ? { maxAge: '365d', immutable: true } : { maxAge: 0 });
|
|
636
651
|
}
|
|
637
652
|
} else {
|
|
638
653
|
res.sendFile('/images/og-image.jpg', { root: staticDir, maxAge: '1h' });
|
package/api/routes/csp-proxy.js
CHANGED
|
@@ -1,52 +1,160 @@
|
|
|
1
|
-
const { getChainClient } = require('@abtnode/util/lib/get-chain-client');
|
|
2
|
-
const { MAIN_CHAIN_ENDPOINT } = require('@abtnode/constant');
|
|
3
1
|
const { WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
|
|
4
2
|
const { default: axios } = require('axios');
|
|
5
3
|
const isUrl = require('is-url');
|
|
4
|
+
const isPrivateIP = require('private-ip');
|
|
6
5
|
|
|
7
6
|
const logger = require('../libs/logger')('blocklet-services:csp-proxy');
|
|
8
7
|
|
|
9
8
|
const PREFIX = WELLKNOWN_SERVICE_PATH_PREFIX;
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// 允许的图片内容类型和 JSON 内容类型
|
|
11
|
+
const ALLOWED_CONTENT_TYPES = [
|
|
12
|
+
'image/jpeg',
|
|
13
|
+
'image/png',
|
|
14
|
+
'image/gif',
|
|
15
|
+
'image/webp',
|
|
16
|
+
'image/svg+xml',
|
|
17
|
+
'image/avif',
|
|
18
|
+
'image/bmp',
|
|
19
|
+
'image/x-icon',
|
|
20
|
+
'application/json',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 检查请求来源是否合法
|
|
25
|
+
*/
|
|
26
|
+
function checkReferer(req) {
|
|
27
|
+
const referer = req.get('referer');
|
|
28
|
+
if (!referer) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const refererUrl = new URL(referer);
|
|
34
|
+
const currentHost = req.get('host');
|
|
14
35
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
// 检查referer是否来自当前站点
|
|
37
|
+
return refererUrl.hostname === currentHost || refererUrl.hostname === currentHost.split(':')[0]; // 处理端口号情况
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 检查内容类型是否为允许的图片类型
|
|
45
|
+
*/
|
|
46
|
+
function isAllowedContentType(contentType) {
|
|
47
|
+
if (!contentType) return false;
|
|
48
|
+
const lowerContentType = contentType.toLowerCase();
|
|
49
|
+
return ALLOWED_CONTENT_TYPES.some((type) => lowerContentType.startsWith(type));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
init(server) {
|
|
20
54
|
server.get(`${PREFIX}/proxy`, async (req, res) => {
|
|
21
55
|
const { url } = req.query;
|
|
22
56
|
|
|
23
57
|
if (!url) {
|
|
24
|
-
res.status(400).send('
|
|
58
|
+
res.status(400).send('Invalid parameter');
|
|
59
|
+
return;
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
if (!isUrl(url)) {
|
|
28
|
-
res.status(400).send('Invalid
|
|
63
|
+
res.status(400).send('Invalid parameter');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 检查referer来源
|
|
68
|
+
if (!checkReferer(req)) {
|
|
69
|
+
res.status(403).send('Invalid parameter');
|
|
29
70
|
return;
|
|
30
71
|
}
|
|
31
72
|
|
|
32
73
|
try {
|
|
33
74
|
const tmp = new URL(url);
|
|
34
75
|
if (tmp.protocol !== 'https:') {
|
|
35
|
-
res.status(400).send('Invalid
|
|
76
|
+
res.status(400).send('Invalid parameter');
|
|
36
77
|
return;
|
|
37
78
|
}
|
|
38
79
|
|
|
39
|
-
//
|
|
80
|
+
// 检查是否为内网地址,防止SSRF攻击
|
|
81
|
+
if (isPrivateIP(tmp.hostname)) {
|
|
82
|
+
res.status(400).send('Invalid parameter');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 配置axios请求,确保不传递用户的cookie等认证信息
|
|
87
|
+
const requestConfig = {
|
|
88
|
+
maxRedirects: 0,
|
|
89
|
+
headers: {
|
|
90
|
+
'User-Agent': 'BlockletServer-CSP-Proxy/1.0',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// 直接发起GET请求,在响应头返回后立即检查content-type
|
|
40
95
|
const response = await axios.get(url, {
|
|
96
|
+
...requestConfig,
|
|
41
97
|
responseType: 'stream',
|
|
42
98
|
});
|
|
43
99
|
|
|
100
|
+
// 立即检查响应的content-type
|
|
101
|
+
const contentType = response.headers['content-type'];
|
|
102
|
+
if (!isAllowedContentType(contentType)) {
|
|
103
|
+
// 立即销毁stream并返回错误,避免继续下载
|
|
104
|
+
response.data.destroy();
|
|
105
|
+
res.status(400).send('Invalid parameter');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
44
109
|
// 设置响应头
|
|
45
110
|
res.set('Content-Type', response.headers['content-type']);
|
|
46
111
|
if (response.headers['content-length']) {
|
|
47
112
|
res.set('Content-Length', response.headers['content-length']);
|
|
48
113
|
}
|
|
49
114
|
|
|
115
|
+
// 关键安全头:防止第三方内容获取本站cookie
|
|
116
|
+
const securityHeaders = {
|
|
117
|
+
// 防止浏览器MIME类型嗅探,严格按content-type处理
|
|
118
|
+
'X-Content-Type-Options': 'nosniff',
|
|
119
|
+
// 禁止发送referrer信息,保护隐私
|
|
120
|
+
'Referrer-Policy': 'no-referrer',
|
|
121
|
+
// 防止XSS攻击
|
|
122
|
+
'X-XSS-Protection': '1; mode=block',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 根据内容类型设置不同的安全策略
|
|
126
|
+
const lowerContentType = contentType.toLowerCase();
|
|
127
|
+
|
|
128
|
+
if (lowerContentType.startsWith('application/json')) {
|
|
129
|
+
// JSON响应:严格禁止执行和嵌入
|
|
130
|
+
securityHeaders['Content-Security-Policy'] = "default-src 'none'; script-src 'none'; object-src 'none';";
|
|
131
|
+
securityHeaders['X-Frame-Options'] = 'DENY';
|
|
132
|
+
} else if (lowerContentType.startsWith('image/svg+xml')) {
|
|
133
|
+
// SVG图片:禁止脚本但允许样式,可能需要嵌入使用
|
|
134
|
+
securityHeaders['Content-Security-Policy'] =
|
|
135
|
+
"default-src 'none'; script-src 'none'; style-src 'unsafe-inline';";
|
|
136
|
+
securityHeaders['X-Frame-Options'] = 'SAMEORIGIN'; // 允许同源嵌入
|
|
137
|
+
} else if (lowerContentType.startsWith('image/')) {
|
|
138
|
+
// 普通图片:最宽松的策略,但仍防止脚本执行
|
|
139
|
+
securityHeaders['Content-Security-Policy'] = "script-src 'none';";
|
|
140
|
+
// 不设置X-Frame-Options,允许嵌入
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 仅在HTTPS环境下设置HSTS
|
|
144
|
+
if (req.secure || req.get('x-forwarded-proto') === 'https') {
|
|
145
|
+
securityHeaders['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
res.set(securityHeaders);
|
|
149
|
+
|
|
150
|
+
// 添加缓存控制头
|
|
151
|
+
res.set('Cache-Control', 'public, max-age=3600'); // 缓存1小时
|
|
152
|
+
|
|
153
|
+
// 确保不传递可能包含敏感信息的响应头
|
|
154
|
+
res.removeHeader('Set-Cookie');
|
|
155
|
+
res.removeHeader('Authorization');
|
|
156
|
+
res.removeHeader('WWW-Authenticate');
|
|
157
|
+
|
|
50
158
|
// 直接将流管道传输到响应
|
|
51
159
|
response.data.pipe(res);
|
|
52
160
|
|
|
@@ -59,7 +167,14 @@ module.exports = {
|
|
|
59
167
|
});
|
|
60
168
|
} catch (error) {
|
|
61
169
|
logger.error('Error fetching the resource:', { error, url });
|
|
62
|
-
|
|
170
|
+
|
|
171
|
+
if (error.code === 'ECONNABORTED') {
|
|
172
|
+
res.status(408).send('Request timeout');
|
|
173
|
+
} else if (error.response && error.response.status) {
|
|
174
|
+
res.status(error.response.status).send(`Remote server error: ${error.response.status}`);
|
|
175
|
+
} else {
|
|
176
|
+
res.status(400).send('Invalid parameter');
|
|
177
|
+
}
|
|
63
178
|
}
|
|
64
179
|
});
|
|
65
180
|
},
|
package/api/routes/user.js
CHANGED
|
@@ -988,12 +988,16 @@ module.exports = {
|
|
|
988
988
|
const { token } = req;
|
|
989
989
|
if (token) {
|
|
990
990
|
const sessionTtl = jwtDecode(token).exp * 1000 - Date.now();
|
|
991
|
-
|
|
991
|
+
if (sessionTtl > 0) {
|
|
992
|
+
await cache.sessionToken.set(md5(token), { block: true }, { ttl: sessionTtl });
|
|
993
|
+
}
|
|
992
994
|
}
|
|
993
995
|
// 兼容旧版本,API 不会携带 refreshToken 的情况
|
|
994
996
|
if (refreshToken) {
|
|
995
997
|
const refreshTtl = jwtDecode(refreshToken).exp * 1000 - Date.now();
|
|
996
|
-
|
|
998
|
+
if (refreshTtl > 0) {
|
|
999
|
+
await cache.refreshToken.set(md5(refreshToken), { block: true }, { ttl: refreshTtl });
|
|
1000
|
+
}
|
|
997
1001
|
}
|
|
998
1002
|
} catch (err) {
|
|
999
1003
|
logger.error('Failed to block session token & refresh token', { error: err });
|