diff --git a/develop/README.md b/develop/README.md
index 8e3f89862c..1c2016ab3a 100644
--- a/develop/README.md
+++ b/develop/README.md
@@ -80,6 +80,7 @@ each service:
| `references` | 9238 |
| `history-v1` | 9239 |
| `project-history` | 9240 |
+| `linked-url-proxy` | 9241 |
To attach to a service using Chrome's _remote debugging_, go to
and make sure _Discover network targets_ is checked. Next
diff --git a/develop/dev.env b/develop/dev.env
index db22c4a137..97fbb2bd25 100644
--- a/develop/dev.env
+++ b/develop/dev.env
@@ -7,6 +7,7 @@ FILESTORE_HOST=filestore
GRACEFUL_SHUTDOWN_DELAY_SECONDS=0
HISTORY_V1_HOST=history-v1
HISTORY_REDIS_HOST=redis
+LINKED_URL_PROXY_HOST=linked-url-proxy
LISTEN_ADDRESS=0.0.0.0
MONGO_HOST=mongo
MONGO_URL=mongodb://mongo/sharelatex?directConnection=true
diff --git a/develop/docker-compose.dev.yml b/develop/docker-compose.dev.yml
index c72d91d073..be140fd06c 100644
--- a/develop/docker-compose.dev.yml
+++ b/develop/docker-compose.dev.yml
@@ -79,6 +79,17 @@ services:
- ../services/history-v1/knexfile.js:/overleaf/services/history-v1/knexfile.js
- ../services/history-v1/migrations:/overleaf/services/history-v1/migrations
+ linked-url-proxy:
+ command: ["node", "--watch", "app.mjs"]
+ environment:
+ - NODE_OPTIONS=--inspect=0.0.0.0:9229
+ ports:
+ - "127.0.0.1:9241:9229"
+ volumes:
+ - ../services/linked-url-proxy/app:/overleaf/services/linked-url-proxy/app
+ - ../services/linked-url-proxy/config:/overleaf/services/linked-url-proxy/config
+ - ../services/linked-url-proxy/app.js:/overleaf/services/linked-url-proxy/app.js
+
notifications:
command: ["node", "--watch", "app.ts"]
environment:
diff --git a/develop/docker-compose.yml b/develop/docker-compose.yml
index 7d5d287d76..c232ff2636 100644
--- a/develop/docker-compose.yml
+++ b/develop/docker-compose.yml
@@ -86,6 +86,13 @@ services:
volumes:
- history-v1-buckets:/buckets
+ linked-url-proxy:
+ build:
+ context: ..
+ dockerfile: services/linked-url-proxy/Dockerfile
+ env_file:
+ - dev.env
+
mongo:
image: mongo:6.0
command: --replSet overleaf
@@ -164,6 +171,7 @@ services:
- document-updater
- filestore
- history-v1
+ - linked-url-proxy
- notifications
- project-history
- real-time
diff --git a/server-ce/config/env.sh b/server-ce/config/env.sh
index 81cebe4caa..76d8c0f142 100644
--- a/server-ce/config/env.sh
+++ b/server-ce/config/env.sh
@@ -6,6 +6,7 @@ export DOCUMENT_UPDATER_HOST=127.0.0.1
export DOCUPDATER_HOST=127.0.0.1
export FILESTORE_HOST=127.0.0.1
export HISTORY_V1_HOST=127.0.0.1
+export LINKED_URL_PROXY_HOST=127.0.0.1
export NOTIFICATIONS_HOST=127.0.0.1
export PROJECT_HISTORY_HOST=127.0.0.1
export REALTIME_HOST=127.0.0.1
diff --git a/server-ce/runit/linked-url-proxy-overleaf/run b/server-ce/runit/linked-url-proxy-overleaf/run
new file mode 100755
index 0000000000..e628c4b027
--- /dev/null
+++ b/server-ce/runit/linked-url-proxy-overleaf/run
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+NODE_PARAMS=""
+if [ "$DEBUG_NODE" == "true" ]; then
+ echo "running debug - linked-url-proxy"
+ NODE_PARAMS="--inspect=0.0.0.0:30660"
+fi
+
+source /etc/overleaf/env.sh
+export LISTEN_ADDRESS=127.0.0.1
+
+exec /sbin/setuser www-data /usr/bin/node $NODE_PARAMS /overleaf/services/linked-url-proxy/app.mjs >> /var/log/overleaf/linked-url-proxy.log 2>&1
diff --git a/server-ce/services.js b/server-ce/services.js
index e0282f3bad..15104dfe28 100644
--- a/server-ce/services.js
+++ b/server-ce/services.js
@@ -35,6 +35,9 @@ module.exports = [
{
name: 'history-v1',
},
+ {
+ name: 'linked-url-proxy',
+ },
]
if (require.main === module) {
diff --git a/services/linked-url-proxy/.nvmrc b/services/linked-url-proxy/.nvmrc
new file mode 100644
index 0000000000..91d5f6ff8e
--- /dev/null
+++ b/services/linked-url-proxy/.nvmrc
@@ -0,0 +1 @@
+22.18.0
diff --git a/services/linked-url-proxy/Dockerfile b/services/linked-url-proxy/Dockerfile
new file mode 100644
index 0000000000..677006182c
--- /dev/null
+++ b/services/linked-url-proxy/Dockerfile
@@ -0,0 +1,32 @@
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/overleaf/internal/
+
+FROM node:20.18.2 AS base
+
+WORKDIR /overleaf/services/linked-url-proxy
+
+# Google Cloud Storage needs a writable $HOME/.config for resumable uploads
+# (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream)
+RUN mkdir /home/node/.config && chown node:node /home/node/.config
+
+FROM base AS app
+
+COPY package.json package-lock.json /overleaf/
+COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
+COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
+COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
+COPY services/linked-url-proxy/package.json /overleaf/services/linked-url-proxy/package.json
+COPY patches/ /overleaf/patches/
+
+RUN cd /overleaf && npm ci --quiet
+
+COPY libraries/logger/ /overleaf/libraries/logger/
+COPY libraries/metrics/ /overleaf/libraries/metrics/
+COPY libraries/settings/ /overleaf/libraries/settings/
+COPY services/linked-url-proxy/ /overleaf/services/linked-url-proxy/
+
+FROM app
+USER node
+
+CMD ["node", "--expose-gc", "app.mjs"]
diff --git a/services/linked-url-proxy/LICENSE b/services/linked-url-proxy/LICENSE
new file mode 100644
index 0000000000..ac8619dcb9
--- /dev/null
+++ b/services/linked-url-proxy/LICENSE
@@ -0,0 +1,662 @@
+
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/services/linked-url-proxy/Makefile b/services/linked-url-proxy/Makefile
new file mode 100644
index 0000000000..80a6bbccb1
--- /dev/null
+++ b/services/linked-url-proxy/Makefile
@@ -0,0 +1,174 @@
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/overleaf/internal/
+
+BUILD_NUMBER ?= local
+BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
+PROJECT_NAME = linked-url-proxy
+BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
+HERE=$(shell pwd)
+export MONOREPO ?= $(shell cd ../../ && pwd)
+IMAGE_CI ?= ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
+IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
+IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME)-$(BUILD_NUMBER)
+IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
+ $(MONOREPO)/package.json \
+ $(MONOREPO)/package-lock.json \
+ $(MONOREPO)/libraries/fetch-utils/package.json \
+ $(MONOREPO)/libraries/logger/package.json \
+ $(MONOREPO)/libraries/metrics/package.json \
+ $(MONOREPO)/libraries/o-error/package.json \
+ $(MONOREPO)/libraries/overleaf-editor-core/package.json \
+ $(MONOREPO)/libraries/promise-utils/package.json \
+ $(MONOREPO)/libraries/settings/package.json \
+ $(MONOREPO)/libraries/stream-utils/package.json \
+ $(MONOREPO)/services/linked-url-proxy/package.json \
+ $(MONOREPO)/patches/* \
+| sha256sum | cut -d '-' -f1)
+
+DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
+DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \
+ BRANCH_NAME=$(BRANCH_NAME) \
+ PROJECT_NAME=$(PROJECT_NAME) \
+ MOCHA_GREP=${MOCHA_GREP} \
+ docker compose ${DOCKER_COMPOSE_FLAGS}
+
+COMPOSE_PROJECT_NAME_TEST_ACCEPTANCE ?= test_acceptance_$(BUILD_DIR_NAME)
+DOCKER_COMPOSE_TEST_ACCEPTANCE = \
+ COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME_TEST_ACCEPTANCE) $(DOCKER_COMPOSE)
+
+COMPOSE_PROJECT_NAME_TEST_UNIT ?= test_unit_$(BUILD_DIR_NAME)
+DOCKER_COMPOSE_TEST_UNIT = \
+ COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME_TEST_UNIT) $(DOCKER_COMPOSE)
+
+clean:
+ -docker rmi $(IMAGE_CI)
+ -docker rmi $(IMAGE_REPO_FINAL)
+ -$(DOCKER_COMPOSE_TEST_UNIT) down --remove-orphans --rmi local --timeout 0 --volumes
+ -$(DOCKER_COMPOSE_TEST_ACCEPTANCE) down --remove-orphans --rmi local --timeout 0 --volumes
+ -rm -rf reports/
+
+# Run the linting commands in the scope of the monorepo.
+# Eslint and prettier (plus some configs) are on the root.
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) node:22.18.0 npm run --silent
+
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/linked-url-proxy/reports:/overleaf/services/linked-url-proxy/reports $(IMAGE_CI) npm run --silent
+
+# Same but from the top of the monorepo
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) node:22.18.0 npm run --silent
+
+SHELLCHECK_OPTS = \
+ --shell=bash \
+ --external-sources
+SHELLCHECK_COLOR := $(if $(CI),--color=never,--color)
+SHELLCHECK_FILES := { git ls-files "*.sh" -z; git grep -Plz "\A\#\!.*bash"; } | sort -zu
+
+shellcheck:
+ @$(SHELLCHECK_FILES) | xargs -0 -r docker run --rm -v $(HERE):/mnt -w /mnt \
+ koalaman/shellcheck:stable $(SHELLCHECK_OPTS) $(SHELLCHECK_COLOR)
+
+shellcheck_fix:
+ @$(SHELLCHECK_FILES) | while IFS= read -r -d '' file; do \
+ diff=$$(docker run --rm -v $(HERE):/mnt -w /mnt koalaman/shellcheck:stable $(SHELLCHECK_OPTS) --format=diff "$$file" 2>/dev/null); \
+ if [ -n "$$diff" ] && ! echo "$$diff" | patch -p1 >/dev/null 2>&1; then echo "\033[31m$$file\033[0m"; \
+ elif [ -n "$$diff" ]; then echo "$$file"; \
+ else echo "\033[2m$$file\033[0m"; fi \
+ done
+
+format:
+ $(RUN_LINTING) format
+
+format_ci:
+ $(RUN_LINTING_CI) format
+
+format_fix:
+ $(RUN_LINTING) format:fix
+
+lint:
+ $(RUN_LINTING) lint
+
+lint_ci:
+ -$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+ sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
+
+lint_fix:
+ $(RUN_LINTING) lint:fix
+
+typecheck:
+ $(RUN_LINTING) types:check
+
+typecheck_ci:
+ $(RUN_LINTING_CI) types:check
+
+test: format lint typecheck shellcheck test_unit test_acceptance
+
+test_unit:
+ifneq (,$(wildcard test/unit))
+ $(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
+endif
+
+test_clean: test_unit_clean
+test_unit_clean:
+ifneq (,$(wildcard test/unit))
+ $(DOCKER_COMPOSE_TEST_UNIT) down -v -t 0
+endif
+
+test_acceptance: test_acceptance_clean test_acceptance_pre_run test_acceptance_run
+ $(MAKE) test_acceptance_clean
+
+test_acceptance_debug: test_acceptance_clean test_acceptance_pre_run test_acceptance_run_debug
+ $(MAKE) test_acceptance_clean
+
+test_acceptance_run:
+ifneq (,$(wildcard test/acceptance))
+ $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance
+endif
+
+test_acceptance_run_debug:
+ifneq (,$(wildcard test/acceptance))
+ $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+endif
+
+test_clean: test_acceptance_clean
+test_acceptance_clean:
+ $(DOCKER_COMPOSE_TEST_ACCEPTANCE) down -v -t 0
+
+test_acceptance_pre_run:
+ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
+ $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance test/acceptance/js/scripts/pre-run
+endif
+
+benchmarks:
+ $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+
+build:
+ docker build \
+ --pull \
+ --build-arg BUILDKIT_INLINE_CACHE=1 \
+ --tag $(IMAGE_CI) \
+ --tag $(IMAGE_CACHE) \
+ --tag $(IMAGE_REPO_FINAL) \
+ --cache-from $(IMAGE_CACHE) \
+ --file Dockerfile \
+ ../..
+
+tar:
+ $(DOCKER_COMPOSE) up tar
+
+push:
+ docker push $(IMAGE_REPO_FINAL)
+
+push_branch:
+ docker push $(IMAGE_CACHE)
+
+.PHONY: clean \
+ format format_fix \
+ lint lint_fix \
+ build_types typecheck \
+ lint_ci format_ci typecheck_ci \
+ shellcheck shellcheck_fix \
+ test test_clean test_unit test_unit_clean \
+ test_acceptance test_acceptance_debug test_acceptance_pre_run \
+ test_acceptance_run test_acceptance_run_debug test_acceptance_clean \
+ benchmarks \
+ build tar publish \
diff --git a/services/linked-url-proxy/README.md b/services/linked-url-proxy/README.md
new file mode 100644
index 0000000000..3300e73314
--- /dev/null
+++ b/services/linked-url-proxy/README.md
@@ -0,0 +1,10 @@
+overleaf/linked-url-proxy
+===============
+
+An API for providing linked url proxy
+
+License
+=======
+The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3.
+
+Copyright (c) yu-i-i (https://github.com/yu-i-i/overleaf-cep), 2025.
diff --git a/services/linked-url-proxy/app.mjs b/services/linked-url-proxy/app.mjs
new file mode 100644
index 0000000000..499a23f35e
--- /dev/null
+++ b/services/linked-url-proxy/app.mjs
@@ -0,0 +1,39 @@
+import '@overleaf/metrics/initialize.js'
+
+import express from 'express'
+import Settings from '@overleaf/settings'
+import logger from '@overleaf/logger'
+import Metrics from '@overleaf/metrics'
+import LinkedUrlProxyController from './app/js/LinkedUrlProxyController.mjs'
+
+Metrics.open_sockets.monitor(true)
+Metrics.memory.monitor(logger)
+Metrics.leaked_sockets.monitor(logger)
+
+const app = express()
+
+Metrics.injectMetricsRoute(app)
+app.use(Metrics.http.monitor(logger))
+
+app.get('/', LinkedUrlProxyController.proxy)
+app.get('/status', (req, res) => res.send({ status: 'linked-url-proxy is up' }))
+
+const host = Settings.internal.linkedUrlProxy.host
+const port = Settings.internal.linkedUrlProxy.port
+
+logger.debug('Listening at', { host, port })
+
+const server = app.listen(port, host, function (error) {
+ if (error) {
+ throw error
+ }
+ logger.info({ host, port }, 'linked-url-proxy HTTP server starting up')
+})
+
+process.on('SIGTERM', () => {
+ server.close(() => {
+ logger.info({ host, port }, 'linked-url-proxy HTTP server closed')
+ metrics.close()
+ })
+})
+
diff --git a/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
new file mode 100644
index 0000000000..1269eede0d
--- /dev/null
+++ b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
@@ -0,0 +1,180 @@
+import dns from 'dns/promises'
+import ipaddr from 'ipaddr.js'
+import { URL } from 'node:url'
+import { Transform } from 'node:stream'
+import logger from '@overleaf/logger'
+import Settings from '@overleaf/settings'
+import { fetchStreamWithResponse, RequestFailedError } from '@overleaf/fetch-utils'
+
+function isAllowedResource(targetUrl) {
+ if (!Settings.allowedResources) return false
+ return Settings.allowedResources.test(targetUrl)
+}
+
+function isBlockedIp(ipStr, targetUrl) {
+ const addr = ipaddr.parse(ipStr)
+ if (addr.kind() === 'ipv6' && addr.isIPv4MappedAddress()) {
+ return isBlockedIp(addr.toIPv4Address().toString(), targetUrl)
+ }
+
+ const range = addr.range()
+ if ([
+ 'loopback',
+ 'private',
+ 'linkLocal',
+ 'multicast',
+ 'reserved',
+ 'broadcast',
+ 'unspecified'
+ ].includes(range)) {
+ return true
+ }
+
+ for (const blocked of Settings.blockedNetworks) {
+ try {
+ const net = ipaddr.parseCIDR(blocked)
+ if (addr.match(net)) return true
+ } catch (e) {
+ logger.error({ blocked, error: e }, 'Invalid blockedNetworks entry')
+ const err = new Error(`Invalid blockedNetworks entry: ${blocked}`)
+ err.info = { status: 500 }
+ throw err
+ }
+ }
+ return false
+}
+
+async function validateSourceUrl(hostname, targetUrl) {
+ const records = await dns.lookup(hostname, { all: true }).catch(() => [])
+ if (!records.length) {
+ const err = new Error(`DNS lookup failed for ${hostname}`)
+ err.info = { status: 421 }
+ throw err
+ }
+// Permit explicitly allowed resources without checking blocked IPs
+ if (isAllowedResource(targetUrl)) return
+ for (const { address } of records) {
+ if (isBlockedIp(address, targetUrl)) {
+ const err = new Error(`Blocked IP address: ${address}`)
+ err.info = { status: 403 }
+ throw err
+ }
+ }
+}
+
+async function fetchValidated(urlStr, redirectCount = 0) {
+ if (redirectCount > Settings.maxRedirects) {
+ const err = new Error('Too many redirects')
+ err.info = { status: 421 }
+ throw err
+ }
+
+ const url = new URL(urlStr)
+ if (!['http:', 'https:'].includes(url.protocol)) {
+ const err = new Error(`${url.protocol} protocol is not allowed`)
+ err.info = { status: 400 }
+ throw err
+ }
+
+ // Validate DNS and blocked IPs
+ await validateSourceUrl(url.hostname, urlStr)
+
+ const opts = {
+ redirect: 'manual',
+ timeout: Settings.fetchTimeoutMs,
+ headers: Settings.userAgentHeader,
+ }
+
+ try {
+ const { stream, response } = await fetchStreamWithResponse(urlStr, opts)
+
+ const contentLengthHeader = response.headers.get('content-length')
+ if (contentLengthHeader) {
+ const n = parseInt(contentLengthHeader, 10)
+ if (!Number.isNaN(n) && n > Settings.maxUploadSize) {
+ const err = new Error('file too large')
+ err.info = { status: 413 }
+ try { stream.destroy() } catch (_) {}
+ throw err
+ }
+ }
+
+ return {
+ stream,
+ response,
+ headers: Object.fromEntries(response.headers.entries()),
+ }
+ } catch (err) {
+ if (err instanceof RequestFailedError) {
+ const status = err.info.status
+
+ // Handle redirects
+ if (status >= 300 && status < 400) {
+ const location = err.response.headers.get('Location')
+ if (location) {
+ const nextUrl = new URL(location, url).toString()
+ return fetchValidated(nextUrl, redirectCount + 1)
+ } else {
+ const e = new Error('Redirect response missing Location header')
+ e.info = { status: 421 }
+ throw e
+ }
+ }
+ throw err
+ }
+ if (!err?.info?.status) {
+ if(err.type === "request-timeout") {
+ err.info = { status: 408 }
+ } else err.info = { status: 422 }
+ }
+ throw err
+ }
+}
+
+async function proxy(req, res) {
+ try {
+ const u = new URL(req.url, `http://${req.headers.host}`)
+ const targetUrl = u.searchParams.get('url')
+ if (!targetUrl) {
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
+ res.end('Missing ?url parameter')
+ return
+ }
+
+ const { stream: upstreamStream, response, headers } = await fetchValidated(targetUrl)
+
+ res.statusCode = response.status || 200
+ res.setHeader('Content-Type', headers['content-type'] || 'application/octet-stream')
+ res.setHeader('Cache-Control', 'no-store')
+
+ function onError(err) {
+ logger.warn({ err, url: req.url }, 'linked-url-proxy request failed')
+ try { upstreamStream.destroy() } catch (_) {}
+ if (!res.headersSent) {
+ let body = `Error: ${err?.message ?? String(err)}`
+ res.writeHead(err?.info?.status || 503, { 'Content-Type': 'text/plain' })
+ res.end(body)
+ } else {
+ try { res.destroy() } catch (_) {}
+ }
+ }
+
+ upstreamStream.on('error', onError)
+ upstreamStream.pipe(res)
+
+ } catch (err) {
+ logger.warn({ err, url: req.url }, 'linked-url-proxy request failed')
+
+ let status = err.info.status
+ let body = `Error: ${err.message || String(err)}`
+
+ try {
+ res.writeHead(status, { 'Content-Type': 'text/plain' })
+ res.end(body)
+ } catch {
+ try { res.end() } catch {}
+ }
+ }
+}
+
+export default { proxy }
diff --git a/services/linked-url-proxy/buildscript.txt b/services/linked-url-proxy/buildscript.txt
new file mode 100644
index 0000000000..71bc14c557
--- /dev/null
+++ b/services/linked-url-proxy/buildscript.txt
@@ -0,0 +1,6 @@
+linked-url-proxy
+--env-add=
+--env-pass-through=
+--esmock-loader=False
+--node-version=22.18.0
+--public-repo=False
diff --git a/services/linked-url-proxy/config/settings.defaults.cjs b/services/linked-url-proxy/config/settings.defaults.cjs
new file mode 100644
index 0000000000..7481847af6
--- /dev/null
+++ b/services/linked-url-proxy/config/settings.defaults.cjs
@@ -0,0 +1,27 @@
+const blockedNetworks = (process.env.OVERLEAF_LINKED_URL_BLOCKED_NETWORKS || '')
+ .split(/[,\s]+/)
+ .filter(Boolean)
+ .map(cidr => cidr.trim())
+
+const allowedResources = process.env.OVERLEAF_LINKED_URL_ALLOWED_RESOURCES
+ ? new RegExp(process.env.OVERLEAF_LINKED_URL_ALLOWED_RESOURCES)
+ : null
+
+module.exports = {
+ maxRedirects: 5,
+ fetchTimeoutMs: 30000,
+ blockedNetworks,
+ allowedResources,
+ userAgentHeader: {
+ 'User-Agent': 'Overleaf Extended CE - LinkedURLProxy (https://github.com/yu-i-i/overleaf-cep)'
+ },
+ maxUploadSize: process.env.MAX_UPLOAD_SIZE
+ ? parseInt(process.env.MAX_UPLOAD_SIZE, 10) * 1024 * 1024
+ : 50 * 1024 * 1024, // 50 MB
+ internal: {
+ linkedUrlProxy: {
+ port: 3066,
+ host: process.env.LINKED_URL_PROXY_HOST || '127.0.0.1',
+ },
+ },
+}
diff --git a/services/linked-url-proxy/docker-compose.ci.yml b/services/linked-url-proxy/docker-compose.ci.yml
new file mode 100644
index 0000000000..6373c3ea44
--- /dev/null
+++ b/services/linked-url-proxy/docker-compose.ci.yml
@@ -0,0 +1,32 @@
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/overleaf/internal/
+
+services:
+ test_unit:
+ image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
+ user: node
+ command: npm run test:unit:_run
+ environment:
+ NODE_ENV: test
+ NODE_OPTIONS: "--unhandled-rejections=strict"
+
+ test_acceptance:
+ build: .
+ image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
+ environment:
+ ELASTIC_SEARCH_DSN: es:9200
+ POSTGRES_HOST: postgres
+ MOCHA_GREP: ${MOCHA_GREP}
+ NODE_ENV: test
+ NODE_OPTIONS: "--unhandled-rejections=strict"
+ user: node
+ command: npm run test:acceptance
+
+ tar:
+ build: .
+ image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
+ volumes:
+ - ./:/tmp/build/
+ command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
+ user: root
diff --git a/services/linked-url-proxy/docker-compose.yml b/services/linked-url-proxy/docker-compose.yml
new file mode 100644
index 0000000000..87e784e681
--- /dev/null
+++ b/services/linked-url-proxy/docker-compose.yml
@@ -0,0 +1,36 @@
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/overleaf/internal/
+
+services:
+ test_unit:
+ image: node:22.18.0
+ volumes:
+ - .:/overleaf/services/linked-url-proxy
+ - ../../node_modules:/overleaf/node_modules
+ - ../../libraries:/overleaf/libraries
+ working_dir: /overleaf/services/linked-url-proxy
+ environment:
+ MOCHA_GREP: ${MOCHA_GREP}
+ LOG_LEVEL: ${LOG_LEVEL:-}
+ NODE_ENV: test
+ NODE_OPTIONS: "--unhandled-rejections=strict"
+ command: npm run --silent test:unit
+ user: node
+
+ test_acceptance:
+ image: node:22.18.0
+ volumes:
+ - .:/overleaf/services/linked-url-proxy
+ - ../../node_modules:/overleaf/node_modules
+ - ../../libraries:/overleaf/libraries
+ working_dir: /overleaf/services/linked-url-proxy
+ environment:
+ ELASTIC_SEARCH_DSN: es:9200
+ POSTGRES_HOST: postgres
+ MOCHA_GREP: ${MOCHA_GREP}
+ LOG_LEVEL: ${LOG_LEVEL:-}
+ NODE_ENV: test
+ NODE_OPTIONS: "--unhandled-rejections=strict"
+ user: node
+ command: npm run --silent test:acceptance
diff --git a/services/linked-url-proxy/package.json b/services/linked-url-proxy/package.json
new file mode 100644
index 0000000000..845551c8ef
--- /dev/null
+++ b/services/linked-url-proxy/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@overleaf/linked-url-proxy",
+ "description": "An API for providing linked url proxy",
+ "private": true,
+ "type": "module",
+ "main": "app.mjs",
+ "scripts": {
+ "start": "node app.mjs"
+ },
+ "version": "0.1.0",
+ "dependencies": {
+ "@overleaf/settings": "*",
+ "@overleaf/logger": "*",
+ "@overleaf/metrics": "*",
+ "async": "^3.2.5",
+ "express": "^4.21.2"
+ "ipaddr.js": "^1.9.1"
+ }
+}
diff --git a/services/linked-url-proxy/tsconfig.json b/services/linked-url-proxy/tsconfig.json
new file mode 100644
index 0000000000..c018d6e682
--- /dev/null
+++ b/services/linked-url-proxy/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.backend.json",
+ "include": [
+ "app.js",
+ "app.ts",
+ "app/js/**/*",
+ "benchmarks/**/*",
+ "config/**/*",
+ "scripts/**/*",
+ "test/**/*",
+ "types"
+ ]
+}
diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js
index 573b328802..e81e4b31aa 100644
--- a/services/web/config/settings.defaults.js
+++ b/services/web/config/settings.defaults.js
@@ -261,6 +261,9 @@ module.exports = {
contacts: {
url: `http://${process.env.CONTACTS_HOST || '127.0.0.1'}:3036`,
},
+ linkedUrlProxy: {
+ url: `http://${process.env.LINKED_URL_PROXY_HOST || '127.0.0.1'}:3066`,
+ },
notifications: {
url: `http://${process.env.NOTIFICATIONS_HOST || '127.0.0.1'}:3042`,
},