database.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /*! firebase-admin v12.1.1 */
  2. "use strict";
  3. /*!
  4. * Copyright 2020 Google Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.DatabaseService = void 0;
  20. const url_1 = require("url");
  21. const path = require("path");
  22. const error_1 = require("../utils/error");
  23. const validator = require("../utils/validator");
  24. const api_request_1 = require("../utils/api-request");
  25. const index_1 = require("../utils/index");
  26. const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000;
  27. class DatabaseService {
  28. constructor(app) {
  29. this.databases = {};
  30. if (!validator.isNonNullObject(app) || !('options' in app)) {
  31. throw new error_1.FirebaseDatabaseError({
  32. code: 'invalid-argument',
  33. message: 'First argument passed to admin.database() must be a valid Firebase app instance.',
  34. });
  35. }
  36. this.appInternal = app;
  37. }
  38. get firebaseApp() {
  39. return this.app;
  40. }
  41. /**
  42. * @internal
  43. */
  44. delete() {
  45. if (this.tokenListener) {
  46. this.firebaseApp.INTERNAL.removeAuthTokenListener(this.tokenListener);
  47. clearTimeout(this.tokenRefreshTimeout);
  48. }
  49. const promises = [];
  50. for (const dbUrl of Object.keys(this.databases)) {
  51. const db = this.databases[dbUrl];
  52. promises.push(db.INTERNAL.delete());
  53. }
  54. return Promise.all(promises).then(() => {
  55. this.databases = {};
  56. });
  57. }
  58. /**
  59. * Returns the app associated with this DatabaseService instance.
  60. *
  61. * @returns The app associated with this DatabaseService instance.
  62. */
  63. get app() {
  64. return this.appInternal;
  65. }
  66. getDatabase(url) {
  67. const dbUrl = this.ensureUrl(url);
  68. if (!validator.isNonEmptyString(dbUrl)) {
  69. throw new error_1.FirebaseDatabaseError({
  70. code: 'invalid-argument',
  71. message: 'Database URL must be a valid, non-empty URL string.',
  72. });
  73. }
  74. let db = this.databases[dbUrl];
  75. if (typeof db === 'undefined') {
  76. // eslint-disable-next-line @typescript-eslint/no-var-requires
  77. const rtdb = require('@firebase/database-compat/standalone');
  78. db = rtdb.initStandalone(this.appInternal, dbUrl, (0, index_1.getSdkVersion)()).instance;
  79. const rulesClient = new DatabaseRulesClient(this.app, dbUrl);
  80. db.getRules = () => {
  81. return rulesClient.getRules();
  82. };
  83. db.getRulesJSON = () => {
  84. return rulesClient.getRulesJSON();
  85. };
  86. db.setRules = (source) => {
  87. return rulesClient.setRules(source);
  88. };
  89. this.databases[dbUrl] = db;
  90. }
  91. if (!this.tokenListener) {
  92. this.tokenListener = this.onTokenChange.bind(this);
  93. this.firebaseApp.INTERNAL.addAuthTokenListener(this.tokenListener);
  94. }
  95. return db;
  96. }
  97. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  98. onTokenChange(_) {
  99. const token = this.firebaseApp.INTERNAL.getCachedToken();
  100. if (token) {
  101. const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now();
  102. // If the new token is set to expire soon (unlikely), do nothing. Somebody will eventually
  103. // notice and refresh the token, at which point this callback will fire again.
  104. if (delayMillis > 0) {
  105. this.scheduleTokenRefresh(delayMillis);
  106. }
  107. }
  108. }
  109. scheduleTokenRefresh(delayMillis) {
  110. clearTimeout(this.tokenRefreshTimeout);
  111. this.tokenRefreshTimeout = setTimeout(() => {
  112. this.firebaseApp.INTERNAL.getToken(/*forceRefresh=*/ true)
  113. .catch(() => {
  114. // Ignore the error since this might just be an intermittent failure. If we really cannot
  115. // refresh the token, an error will be logged once the existing token expires and we try
  116. // to fetch a fresh one.
  117. });
  118. }, delayMillis);
  119. }
  120. ensureUrl(url) {
  121. if (typeof url !== 'undefined') {
  122. return url;
  123. }
  124. else if (typeof this.appInternal.options.databaseURL !== 'undefined') {
  125. return this.appInternal.options.databaseURL;
  126. }
  127. throw new error_1.FirebaseDatabaseError({
  128. code: 'invalid-argument',
  129. message: 'Can\'t determine Firebase Database URL.',
  130. });
  131. }
  132. }
  133. exports.DatabaseService = DatabaseService;
  134. const RULES_URL_PATH = '.settings/rules.json';
  135. /**
  136. * A helper client for managing RTDB security rules.
  137. */
  138. class DatabaseRulesClient {
  139. constructor(app, dbUrl) {
  140. let parsedUrl = new url_1.URL(dbUrl);
  141. const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST;
  142. if (emulatorHost) {
  143. const namespace = extractNamespace(parsedUrl);
  144. parsedUrl = new url_1.URL(`http://${emulatorHost}?ns=${namespace}`);
  145. }
  146. parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH);
  147. this.dbUrl = parsedUrl.toString();
  148. this.httpClient = new api_request_1.AuthorizedHttpClient(app);
  149. }
  150. /**
  151. * Gets the currently applied security rules as a string. The return value consists of
  152. * the rules source including comments.
  153. *
  154. * @returns A promise fulfilled with the rules as a raw string.
  155. */
  156. getRules() {
  157. const req = {
  158. method: 'GET',
  159. url: this.dbUrl,
  160. };
  161. return this.httpClient.send(req)
  162. .then((resp) => {
  163. if (!resp.text) {
  164. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.');
  165. }
  166. return resp.text;
  167. })
  168. .catch((err) => {
  169. throw this.handleError(err);
  170. });
  171. }
  172. /**
  173. * Gets the currently applied security rules as a parsed JSON object. Any comments in
  174. * the original source are stripped away.
  175. *
  176. * @returns {Promise<object>} A promise fulfilled with the parsed rules source.
  177. */
  178. getRulesJSON() {
  179. const req = {
  180. method: 'GET',
  181. url: this.dbUrl,
  182. data: { format: 'strict' },
  183. };
  184. return this.httpClient.send(req)
  185. .then((resp) => {
  186. return resp.data;
  187. })
  188. .catch((err) => {
  189. throw this.handleError(err);
  190. });
  191. }
  192. /**
  193. * Sets the specified rules on the Firebase Database instance. If the rules source is
  194. * specified as a string or a Buffer, it may include comments.
  195. *
  196. * @param {string|Buffer|object} source Source of the rules to apply. Must not be `null`
  197. * or empty.
  198. * @returns {Promise<void>} Resolves when the rules are set on the Database.
  199. */
  200. setRules(source) {
  201. if (!validator.isNonEmptyString(source) &&
  202. !validator.isBuffer(source) &&
  203. !validator.isNonNullObject(source)) {
  204. const error = new error_1.FirebaseDatabaseError({
  205. code: 'invalid-argument',
  206. message: 'Source must be a non-empty string, Buffer or an object.',
  207. });
  208. return Promise.reject(error);
  209. }
  210. const req = {
  211. method: 'PUT',
  212. url: this.dbUrl,
  213. data: source,
  214. headers: {
  215. 'content-type': 'application/json; charset=utf-8',
  216. },
  217. };
  218. return this.httpClient.send(req)
  219. .then(() => {
  220. return;
  221. })
  222. .catch((err) => {
  223. throw this.handleError(err);
  224. });
  225. }
  226. handleError(err) {
  227. if (err instanceof api_request_1.HttpError) {
  228. return new error_1.FirebaseDatabaseError({
  229. code: error_1.AppErrorCodes.INTERNAL_ERROR,
  230. message: this.getErrorMessage(err),
  231. });
  232. }
  233. return err;
  234. }
  235. getErrorMessage(err) {
  236. const intro = 'Error while accessing security rules';
  237. try {
  238. const body = err.response.data;
  239. if (body && body.error) {
  240. return `${intro}: ${body.error.trim()}`;
  241. }
  242. }
  243. catch {
  244. // Ignore parsing errors
  245. }
  246. return `${intro}: ${err.response.text}`;
  247. }
  248. }
  249. function extractNamespace(parsedUrl) {
  250. const ns = parsedUrl.searchParams.get('ns');
  251. if (ns) {
  252. return ns;
  253. }
  254. const hostname = parsedUrl.hostname;
  255. const dotIndex = hostname.indexOf('.');
  256. return hostname.substring(0, dotIndex).toLowerCase();
  257. }