├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── backend │ ├── app.js │ ├── cloudflare-tunnel.js │ └── package.json └── frontend │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── components.json │ ├── index.html │ ├── package.json │ ├── public │ ├── cloudflare.ico │ └── vite.svg │ ├── src │ ├── App.vue │ ├── assets │ │ ├── cloudflare.png │ │ ├── index.css │ │ └── vue.svg │ ├── components │ │ └── ui │ │ │ ├── alert │ │ │ ├── Alert.vue │ │ │ ├── AlertDescription.vue │ │ │ ├── AlertTitle.vue │ │ │ └── index.ts │ │ │ ├── button │ │ │ ├── Button.vue │ │ │ └── index.ts │ │ │ ├── card │ │ │ ├── Card.vue │ │ │ ├── CardContent.vue │ │ │ ├── CardDescription.vue │ │ │ ├── CardFooter.vue │ │ │ ├── CardHeader.vue │ │ │ ├── CardTitle.vue │ │ │ └── index.ts │ │ │ ├── dialog │ │ │ ├── Dialog.vue │ │ │ ├── DialogClose.vue │ │ │ ├── DialogContent.vue │ │ │ ├── DialogDescription.vue │ │ │ ├── DialogFooter.vue │ │ │ ├── DialogHeader.vue │ │ │ ├── DialogScrollContent.vue │ │ │ ├── DialogTitle.vue │ │ │ ├── DialogTrigger.vue │ │ │ └── index.ts │ │ │ ├── hover-card │ │ │ ├── HoverCard.vue │ │ │ ├── HoverCardContent.vue │ │ │ ├── HoverCardTrigger.vue │ │ │ └── index.ts │ │ │ ├── input │ │ │ ├── Input.vue │ │ │ └── index.ts │ │ │ ├── select │ │ │ ├── Select.vue │ │ │ ├── SelectContent.vue │ │ │ ├── SelectGroup.vue │ │ │ ├── SelectItem.vue │ │ │ ├── SelectItemText.vue │ │ │ ├── SelectLabel.vue │ │ │ ├── SelectScrollDownButton.vue │ │ │ ├── SelectScrollUpButton.vue │ │ │ ├── SelectSeparator.vue │ │ │ ├── SelectTrigger.vue │ │ │ ├── SelectValue.vue │ │ │ └── index.ts │ │ │ ├── separator │ │ │ ├── Separator.vue │ │ │ └── index.ts │ │ │ └── sonner │ │ │ ├── Sonner.vue │ │ │ └── index.ts │ ├── i18n.ts │ ├── lib │ │ └── utils.ts │ ├── locales │ │ ├── de.ts │ │ ├── en.ts │ │ ├── es-BR.ts │ │ ├── fr.ts │ │ ├── ja.ts │ │ ├── ru.ts │ │ └── zh-CN.ts │ ├── main.ts │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── dev ├── Dockerfile ├── README.md ├── docker-compose.yml ├── down.sh └── up.sh ├── screenshot-1.png └── screenshot-2.png /.dockerignore: -------------------------------------------------------------------------------- 1 | app/frontend/node_modules 2 | app/frontend/package-lock.json 3 | app/backend/node_modules 4 | app/backend/package-lock.json 5 | app/frontend/.vscode 6 | app/frontend/dist -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [WisdomSky] 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v4 15 | - 16 | name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | - 19 | name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | - 22 | name: Login to Docker Hub 23 | uses: docker/login-action@v3 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | - 28 | name: Login to GitHub Container Registry 29 | uses: docker/login-action@v3 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.repository_owner }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - 36 | name: Extract version from Dockerfile 37 | run: | 38 | CLOUDFLARED_VERSION=$(grep -oP '(?<=ARG CLOUDFLARED_VERSION=).*' Dockerfile) 39 | echo "version is $CLOUDFLARED_VERSION" 40 | echo "DOCKER_TAG=$CLOUDFLARED_VERSION" >> $GITHUB_ENV 41 | 42 | - 43 | name: Build and push 44 | uses: docker/build-push-action@v5 45 | with: 46 | context: . 47 | platforms: linux/amd64,linux/arm64,linux/armhf, linux/arm 48 | provenance: false 49 | push: true 50 | tags: | 51 | wisdomsky/cloudflared-web:${{ env.DOCKER_TAG }}, 52 | wisdomsky/cloudflared-web:latest, 53 | ghcr.io/wisdomsky/cloudflared-web:${{ env.DOCKER_TAG }}, 54 | ghcr.io/wisdomsky/cloudflared-web:latest 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/frontend/node_modules 2 | app/frontend/package-lock.json 3 | app/backend/node_modules 4 | app/backend/package-lock.json 5 | app/frontend/.vscode 6 | app/frontend/dist 7 | dev/config 8 | dev/.cloudflared 9 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-bookworm-slim 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | ARG TARGETVARIANT 6 | 7 | ARG CLOUDFLARED_VERSION=2025.4.2 8 | ARG CLOUDFLARED_BASE_URL="https://github.com/cloudflare/cloudflared/releases/download" 9 | 10 | ENV VERSION=$CLOUDFLARED_VERSION 11 | ENV WEBUI_PORT=${WEBUI_PORT:-14333} 12 | ENV METRICS_ENABLE=${METRICS_ENABLE:-"false"} 13 | ENV METRICS_PORT=${METRICS_PORT:-60123} 14 | 15 | ENV EDGE_IP_VERSION=auto 16 | ENV PROTOCOL=auto 17 | 18 | EXPOSE ${WEBUI_PORT} 19 | EXPOSE ${METRICS_PORT} 20 | 21 | USER root 22 | WORKDIR /var/app 23 | 24 | RUN apt update && apt upgrade -y && apt install -y curl 25 | 26 | RUN if [ "$TARGETVARIANT" = "v7" ]; then \ 27 | CLOUDFLARED_PKG="cloudflared-$TARGETOS-${TARGETARCH}hf.deb"; \ 28 | else \ 29 | CLOUDFLARED_PKG="cloudflared-$TARGETOS-$TARGETARCH.deb"; \ 30 | fi && \ 31 | curl -L --output cloudflared.deb "$CLOUDFLARED_BASE_URL/$CLOUDFLARED_VERSION/$CLOUDFLARED_PKG" && \ 32 | dpkg -i cloudflared.deb && \ 33 | rm cloudflared.deb 34 | 35 | VOLUME /config 36 | VOLUME /root/.cloudflared 37 | 38 | COPY app/backend /var/app/backend 39 | COPY app/frontend /var/app/frontend 40 | 41 | RUN cd /var/app/frontend && npm install && npm run build 42 | RUN cd /var/app/backend && npm install 43 | 44 | ENTRYPOINT node /var/app/backend/app.js 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Cloudflared-web](https://github.com/WisdomSky/Cloudflared-web) 2 | 3 | _Cloudflared-web is a docker image that packages both cloudflared cli and a simple Web UI to easily start or stop remotely-managed Cloudflare tunnel._ 4 | 5 | 6 | [![build](https://github.com/WisdomSky/Cloudflared-web/workflows/Build/badge.svg)](https://github.com/WisdomSky/Cloudflared-web/actions "Build Status") 7 | [![latest](https://img.shields.io/docker/v/wisdomsky/cloudflared-web/latest?label=Latest)](https://hub.docker.com/r/wisdomsky/cloudflared-web/tags "Latest Tag") 8 | [![pulls](https://img.shields.io/docker/pulls/wisdomsky/cloudflared-web?label=Pulls)](https://hub.docker.com/r/wisdomsky/cloudflared-web "Docker Hub Pulls") 9 | [![stars](https://img.shields.io/docker/stars/wisdomsky/cloudflared-web?label=%E2%AD%90)](https://hub.docker.com/r/wisdomsky/cloudflared-web "Docker Hub Stars") 10 | 11 | --- 12 | 13 | ## Why use `Cloudflared-web`? 14 | 15 | #### Pros 16 | 17 | ✅ Only need to run a docker command once. No need to run docker commands everytime you want to start or stop the container or when updating the token. 18 | 19 | ✅ Start and stop cloudflare tunnel anytime with a single click. 20 | 21 | #### Cons 22 | 23 | ❌ Only supports [Remotely-managed Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/remote-management/). 24 | 25 | ❌ Can only update hostname policies through the [ZeroTrust](https://one.dash.cloudflare.com/) dashboard. 26 | 27 | 28 | --- 29 | ## Application Setup 30 | When manually setting up this image, it is crucial to always set the `networking mode` into `host` as without it, the cloudflared service won't be able to access the services running on the host: 31 | 32 | docker run --network host wisdomsky/cloudflared-web:latest 33 | 34 | or if using `docker-compose.yml`: 35 | 36 | ```yaml 37 | services: 38 | cloudflared: 39 | image: wisdomsky/cloudflared-web:latest 40 | restart: unless-stopped 41 | network_mode: host 42 | ``` 43 | 44 | The Web UI where you can setup the Cloudflared token can be accessed from port `14333`: 45 | 46 | http://localhost:14333 47 | 48 | ### Github Containers 49 | 50 | If for some reason you are unable to pull images from Docker's Official Image Registry (docker.io), `Cloudflared-web` is also synced to Github Container Registry (ghcr.io). 51 | 52 | Just prefix the image with `ghcr.io/` in order to use the mirrored image in Github. 53 | ```yaml 54 | services: 55 | cloudflared: 56 | image: ghcr.io/wisdomsky/cloudflared-web:latest 57 | restart: unless-stopped 58 | network_mode: host 59 | ``` 60 | 61 | 62 | --- 63 | ## Additional Parameters 64 | 65 | ### Environment 66 | | Variable Name | Default value | Required or Optional | Description | 67 | |-------------------|---------------|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 68 | | WEBUI_PORT | 14333 | _Optional_ | The port on the host where the WebUI will be running. Useful when an existing process is running on port `14333` and want to assign cloudflared-web into a different available port. | 69 | | BASIC_AUTH_PASS | | _Optional_ | Enable Basic Auth by specifying a password. If `BASIC_AUTH_USER` is not specified, the default value for username `admin` will be used. | 70 | | BASIC_AUTH_USER | admin | _Optional_ | Specify the username for the Basic Auth. | 71 | | EDGE_BIND_ADDRESS | | _Optional_ | Specifies the outgoing IP address used to establish a connection between `cloudflared` and the Cloudflare global network.

The IP version of `EDGE_BIND_ADDRESS` will override `EDGE_IP_VERSION` (if provided). For example, if you enter an IPv6 source address, `cloudflared` will always connect to an IPv6 destination. | 72 | | EDGE_IP_VERSION | auto | _Optional_ | Specifies the IP address version (IPv4 or IPv6) used to establish a connection between `cloudflared` and the Cloudflare global network. Available values are `auto`, `4`, and `6`. | 73 | | PROTOCOL | auto | _Optional_ | Specifies the protocol used to establish a connection between `cloudflared` and the Cloudflare global network. Available values are `auto`, `http2`, and `quic`. | 74 | | GRACE_PERIOD | 30s | _Optional_ | When `cloudflared` receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shut down. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received. | 75 | | REGION | | _Optional_ | Allows you to choose the regions to which connections are established. Currently the only available value is `us`, which routes all connections through data centers in the United States. Omit or leave empty to connect to the global region. | 76 | | RETRIES | 5 | _Optional_ | Specifies the maximum number of retries for connection/protocol errors. Retries use exponential backoff (retrying at `1`, `2`, `4`, `8`, `16` seconds by default), so it is **NOT RECOMMENDED** that you increase this value significantly. | 77 | | METRICS_ENABLE | false | _Optional_ | Enable [tunnel metrics](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/monitor-tunnels/metrics/) server. | 78 | | METRICS_PORT | 60123 | _Optional_ | Specify port to run tunnel metrics on. `METRICS_ENABLE` must be set to `true`. | 79 | 80 | Based on Cloudflare [tunel run parameters](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/tunnel-run-parameters/) documentation. 81 | 82 | 83 | example `docker-compose.yaml`: 84 | ```yaml 85 | services: 86 | cloudflared: 87 | image: wisdomsky/cloudflared-web:latest 88 | restart: unless-stopped 89 | network_mode: host 90 | environment: 91 | WEBUI_PORT: 1111 92 | PROTOCOL: http2 93 | ``` 94 | 95 | 96 | ### Volume 97 | | Container Path | Required or Optional | Description | 98 | |---|---|---| 99 | | /config | _Optional_ | The path to the directory where the `config.json` file containing the Cloudflare token and start status will be saved. | 100 | 101 | example `docker-compose.yaml`: 102 | ```yaml 103 | services: 104 | cloudflared: 105 | image: wisdomsky/cloudflared-web:latest 106 | restart: unless-stopped 107 | network_mode: host 108 | volumes: 109 | - /mnt/storage/cloudflared/config:/config 110 | ``` 111 | 112 | ## Using Networks 113 | 114 | You can use docker `networks` for a more fine-grained control of which containers/services your cloudflared-web container has access to. 115 | 116 | ```yaml 117 | services: 118 | cloudflared: 119 | image: wisdomsky/cloudflared-web:latest 120 | restart: unless-stopped 121 | networks: 122 | - mynetwork 123 | environment: 124 | WEBUI_PORT: 1111 125 | ``` 126 | 127 | ## Screenshots 128 | 129 | ![Screenshot 1](https://raw.githubusercontent.com/WisdomSky/Cloudflared-web/main/screenshot-1.png) 130 | 131 | ![Screenshot 2](https://raw.githubusercontent.com/WisdomSky/Cloudflared-web/main/screenshot-2.png) 132 | 133 | --- 134 | 135 | ## Issues 136 | 137 | For any problems experienced while using the docker image, please [create a new issue](https://github.com/WisdomSky/Cloudflared-web/issues). 138 | 139 | --- 140 | 141 | ## Contribute 142 | 143 | 144 | ### Adding A Language Translation 145 | 146 | See [Localization](https://github.com/WisdomSky/Cloudflared-web/wiki/Localization). 147 | -------------------------------------------------------------------------------- /app/backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const port = process.env.WEBUI_PORT; 4 | const path = require('path'); 5 | const bodyParser = require('body-parser') 6 | const cors = require('cors'); 7 | const fs = require('node:fs'); 8 | const basicAuth = require('express-basic-auth') 9 | const tmp = require('tmp'); 10 | const { execSync } = require("node:child_process"); 11 | 12 | const { CloudflaredTunnel } = require('./cloudflare-tunnel.js'); 13 | const tunnel = new CloudflaredTunnel(); 14 | 15 | const configpath = "/config/config.json"; 16 | 17 | const cloudflaredconfigdir = "/root/.cloudflared"; 18 | const cloudflaredconfigpath = `${cloudflaredconfigdir}/config.yml`; 19 | 20 | const viewpath = path.normalize(__dirname + '/../frontend/dist'); 21 | 22 | app.use(bodyParser.json()) 23 | app.use(bodyParser.urlencoded({ extended: false })) 24 | app.use(cors()); 25 | 26 | app.use(express.static(viewpath)); 27 | 28 | if (!!process.env.BASIC_AUTH_PASS) { 29 | let users = {}; 30 | 31 | let user = process.env.BASIC_AUTH_USER || 'admin'; 32 | 33 | users[user] = process.env.BASIC_AUTH_PASS; 34 | 35 | app.use(basicAuth({ 36 | users: users, 37 | challenge: true 38 | })) 39 | 40 | } 41 | 42 | 43 | 44 | app.get('/', (req, res) => { 45 | res.sendFile(viewpath + "index.html"); 46 | }) 47 | 48 | app.get('/config', (req, res) => { 49 | let config = getConfig(); 50 | res.status(200).set('Content-Type', 'application/json').send(JSON.stringify(config)); 51 | }) 52 | 53 | app.get('/version', (req, res) => { 54 | let version = execSync('cloudflared -v'); 55 | res.status(200).set('Content-Type', 'text/plain').send(version); 56 | }) 57 | 58 | app.get('/new-version', async (req, res) => { 59 | const current_version = process.env.VERSION; 60 | 61 | let latest_version = current_version; 62 | try { 63 | const resp = await fetch('https://registry.hub.docker.com/v2/repositories/wisdomsky/cloudflared-web/tags/?page_size=100&page=1'); 64 | 65 | const image_info = await resp.json(); 66 | 67 | const tags = image_info.results.filter(tag => tag.name !== 'latest') 68 | 69 | latest_version = tags[0].name; 70 | } catch (e) {} 71 | 72 | res.status(200).set('Content-Type', 'application/json').send({ 73 | current_version, 74 | latest_version, 75 | update: current_version !== latest_version 76 | }); 77 | }) 78 | 79 | app.post('/start', (req, res) => { 80 | 81 | let config = getConfig(); 82 | 83 | const start = req.body.start; 84 | 85 | try { 86 | if (start !== undefined && typeof start === 'boolean') { 87 | config.start = start; 88 | saveConfig(config); 89 | } 90 | 91 | init(config, res); 92 | 93 | } catch(e) {} 94 | res.status(500).send(); 95 | }) 96 | 97 | 98 | app.post('/token', (req, res) => { 99 | 100 | let config = getConfig(); 101 | 102 | const token = req.body.token; 103 | 104 | if (String(token).trim().length === 0) { 105 | res.status(400).send("FAIL: TOKEN REQUIRED!"); 106 | } 107 | 108 | config.token = token; 109 | 110 | saveConfig(config); 111 | try { 112 | res.status(200).send(`OK!`); 113 | } catch(e) {} 114 | console.log(`Token updated: ${token}`); 115 | }) 116 | 117 | 118 | app.get('/advanced/config/local', (req, res) => { 119 | 120 | res.status(200).set('Content-Type', 'text/plain').send(fs.existsSync(cloudflaredconfigpath) ? fs.readFileSync(cloudflaredconfigpath) : ''); 121 | 122 | }) 123 | app.post('/advanced/config/local', (req, res) => { 124 | 125 | const tmpobj = tmp.fileSync(); 126 | 127 | fs.writeFileSync(tmpobj.name, req.body.yaml); 128 | 129 | try { 130 | 131 | if (req.body.yaml.trim().length) { 132 | execSync(`cloudflared --config ${tmpobj.name} tunnel ingress validate`); 133 | } 134 | 135 | if (!fs.existsSync(cloudflaredconfigdir)) { 136 | fs.mkdirSync(cloudflaredconfigdir) 137 | } 138 | 139 | fs.writeFileSync(cloudflaredconfigpath, req.body.yaml); 140 | 141 | tmpobj.removeCallback(); 142 | res.status(200).send('Changes saved into the config file.'); 143 | 144 | } catch (e) { 145 | tmpobj.removeCallback(); 146 | res.status(400).send(e.message.split("\n").splice(1).join("\n")); 147 | } 148 | 149 | 150 | }) 151 | 152 | 153 | app.listen(port, () => { 154 | console.log(`WebUI running on port ${port}`); 155 | let config = getConfig(); 156 | if (config.start) { 157 | console.log('Restarting cloudflare tunnel.'); 158 | init({start: false}); 159 | setTimeout(() => { 160 | init(config); 161 | }, 2000); 162 | } 163 | }) 164 | 165 | 166 | function getConfig() { 167 | let config = { 168 | token: '', 169 | start: false 170 | }; 171 | try { 172 | const json = JSON.parse(fs.readFileSync(configpath)); 173 | config = json; 174 | } catch(e) { 175 | console.log('No pre-existing config file found.'); 176 | } 177 | return config; 178 | } 179 | 180 | function init(config, res) { 181 | tunnel.token = config.token; 182 | try { 183 | if (config.start) { 184 | 185 | let additionalArgs = {} 186 | 187 | if (process.env.METRICS_ENABLE === 'true') { 188 | additionalArgs.metrics = process.env.METRICS_PORT; 189 | } 190 | 191 | if ('EDGE_BIND_ADDRESS' in process.env) { 192 | additionalArgs.edgeBindAddress = process.env.EDGE_BIND_ADDRESS; 193 | } 194 | 195 | if ('GRACE_PERIOD' in process.env) { 196 | additionalArgs.gracePeriod = process.env.GRACE_PERIOD; 197 | } 198 | 199 | if ('REGION' in process.env) { 200 | additionalArgs.region = process.env.REGION; 201 | } 202 | 203 | if ('RETRIES' in process.env) { 204 | additionalArgs.retries = process.env.RETRIES; 205 | } 206 | 207 | if (['4','6'].indexOf(process.env.EDGE_IP_VERSION) !== -1) { 208 | additionalArgs.edgeIpVersion = process.env.EDGE_IP_VERSION; 209 | } 210 | 211 | if (['http2','quic'].indexOf(process.env.PROTOCOL) !== -1) { 212 | additionalArgs.protocol = process.env.PROTOCOL; 213 | } 214 | 215 | if (fs.existsSync(cloudflaredconfigpath)) { 216 | additionalArgs.configPath = cloudflaredconfigpath; 217 | } 218 | 219 | tunnel.start(additionalArgs); 220 | if (res !== undefined) { 221 | res.status(200).send('Started'); 222 | } 223 | console.log(`Cloudflare tunnel started. `); 224 | console.log(`Using token: ${config.token}`); 225 | } else { 226 | tunnel.stop(); 227 | if (res !== undefined) { 228 | res.status(200).send('Stopped'); 229 | } 230 | console.log("Cloudflare tunnel stopped"); 231 | } 232 | } catch (e) { 233 | console.log(e); 234 | } 235 | } 236 | 237 | 238 | function saveConfig(config) { 239 | fs.writeFileSync(configpath, JSON.stringify(config, null, 2) + "\n"); 240 | } -------------------------------------------------------------------------------- /app/backend/cloudflare-tunnel.js: -------------------------------------------------------------------------------- 1 | /** 2 | MIT License 3 | 4 | Copyright (c) 2022 Louis Lam 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | const childProcess = require("child_process"); 26 | const commandExistsSync = require("command-exists").sync; 27 | 28 | class CloudflaredTunnel { 29 | 30 | constructor(cloudflaredPath = "cloudflared") { 31 | this.cloudflaredPath = cloudflaredPath; 32 | 33 | this.url = "http://localhost:80"; 34 | this.hostname = ""; 35 | } 36 | 37 | get token() { 38 | return this._token; 39 | } 40 | 41 | set token(token) { 42 | if (token && typeof token === "string") { 43 | token = token.trim(); 44 | 45 | // try to strip out "cloudflared.exe service install" 46 | let array = token.split(" "); 47 | if (array.length > 1) { 48 | for (let i = 0; i < array.length - 1; i++) { 49 | if (array[i] === "install") { 50 | token = array[i + 1]; 51 | } 52 | } 53 | } 54 | } 55 | 56 | this._token = token; 57 | } 58 | 59 | checkInstalled() { 60 | return commandExistsSync(this.cloudflaredPath); 61 | } 62 | 63 | emitChange(msg, code) { 64 | console.log(msg); 65 | } 66 | 67 | emitError(msg) { 68 | 69 | const imp_err_ids= [ 70 | "ERR Unable to establish connection with Cloudflare edge", 71 | "ERR Failed to fetch features, default to disable error", 72 | "ERR edge discovery: error looking up Cloudflare edge IPs:" 73 | ]; 74 | 75 | if (imp_err_ids.some(subStr => msg.includes(subStr))) { 76 | throw new Error(msg.split('ERR')[1].trim()); 77 | } 78 | 79 | } 80 | 81 | start(additionalArgs = {}) { 82 | if (this.childProcess) { 83 | this.emitError("Already started"); 84 | return; 85 | } 86 | 87 | if (!this.checkInstalled()) { 88 | this.emitError(`Cloudflared error: ${this.cloudflaredPath} is not found`); 89 | return; 90 | } 91 | 92 | if (!this.token) { 93 | this.emitError("Cloudflared error: Token is not set"); 94 | return; 95 | } 96 | 97 | const args = [ 98 | "tunnel", 99 | "--no-autoupdate", 100 | ]; 101 | 102 | if (!!additionalArgs.configPath) { 103 | args.push("--config"); 104 | args.push(additionalArgs.configPath); 105 | } 106 | 107 | if (!!additionalArgs.metrics) { 108 | args.push("--metrics"); 109 | args.push(`0.0.0.0:${additionalArgs.metrics}`); 110 | } 111 | 112 | if (!!additionalArgs.edgeIpVersion) { 113 | args.push("--edge-ip-version"); 114 | args.push(additionalArgs.edgeIpVersion); 115 | } 116 | 117 | if (!!additionalArgs.edgeBindAddress) { 118 | args.push("--edge-bind-address"); 119 | args.push(additionalArgs.edgeBindAddress); 120 | } 121 | 122 | if (!!additionalArgs.gracePeriod) { 123 | args.push("--grace-period"); 124 | args.push(additionalArgs.gracePeriod); 125 | } 126 | 127 | if (!!additionalArgs.region) { 128 | args.push("--region"); 129 | args.push(additionalArgs.region); 130 | } 131 | 132 | if (!!additionalArgs.retries) { 133 | args.push("--retries"); 134 | args.push(additionalArgs.retries); 135 | } 136 | 137 | if (!!additionalArgs.protocol) { 138 | args.push("--protocol"); 139 | args.push(additionalArgs.protocol); 140 | } 141 | 142 | args.push("run"); 143 | args.push("--token"); 144 | args.push(this.token); 145 | 146 | this.emitChange("Starting cloudflared"); 147 | this.childProcess = childProcess.spawn(this.cloudflaredPath, args); 148 | this.childProcess.stdout.pipe(process.stdout); 149 | this.childProcess.stderr.pipe(process.stderr); 150 | 151 | this.childProcess.on("close", (code) => { 152 | this.childProcess = null; 153 | this.emitChange("Stopped cloudflared", code); 154 | }); 155 | 156 | this.childProcess.on("error", (err) => { 157 | if (err.code === "ENOENT") { 158 | this.emitError(`Cloudflared error: ${this.cloudflaredPath} is not found`); 159 | } else { 160 | this.emitError(err); 161 | } 162 | }); 163 | 164 | this.childProcess.stderr.on("data", (data) => { 165 | let msg = data.toString(); 166 | if (!/\s(INF|WRN)\s/g.test(msg)) { 167 | this.emitError(msg); 168 | } 169 | }); 170 | } 171 | 172 | stop() { 173 | this.emitChange("Stopping cloudflared"); 174 | if (this.childProcess) { 175 | this.childProcess.kill("SIGINT"); 176 | this.childProcess = null; 177 | } 178 | } 179 | } 180 | 181 | module.exports = { 182 | CloudflaredTunnel 183 | }; 184 | -------------------------------------------------------------------------------- /app/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "web.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "command-exists": "^1.2.9", 14 | "cors": "^2.8.5", 15 | "express": "^4.18.2", 16 | "express-basic-auth": "^1.2.1", 17 | "tmp": "^0.2.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /app/frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /app/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Cloudflared-web UI 2 | 3 | See `package.json` for commands. -------------------------------------------------------------------------------- /app/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "default", 4 | "typescript": true, 5 | "tsConfigPath": "./tsconfig.json", 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/assets/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "framework": "vite", 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /app/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cloudflare Tunnel WebUI 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "watch": "vite build --watch", 9 | "build": "vue-tsc && vite build", 10 | "preview": "vite preview --host" 11 | }, 12 | "dependencies": { 13 | "@vueuse/core": "^10.11.0", 14 | "class-variance-authority": "^0.7.0", 15 | "clsx": "^2.1.1", 16 | "lucide-vue-next": "^0.396.0", 17 | "prismjs": "^1.29.0", 18 | "radix-vue": "^1.8.5", 19 | "tailwind-merge": "^2.3.0", 20 | "tailwindcss-animate": "^1.0.7", 21 | "vue": "^3.2.47", 22 | "vue-i18n": "^11.0.0-beta.1", 23 | "vue-prism-editor": "^2.0.0-alpha.2", 24 | "vue-sonner": "^1.1.3" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20.14.8", 28 | "@types/prismjs": "^1.26.4", 29 | "@vitejs/plugin-vue": "^4.1.0", 30 | "autoprefixer": "^10.4.19", 31 | "sass": "^1.62.1", 32 | "tailwindcss": "^3.4.4", 33 | "typescript": "5.6.2", 34 | "vite": "^4.3.2", 35 | "vite-plugin-top-level-await": "^1.4.4", 36 | "vue-tsc": "2.0.29" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/frontend/public/cloudflare.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisdomSky/Cloudflared-web/878d8556a9b1fc52d99b07fa67130b9d8e985588/app/frontend/public/cloudflare.ico -------------------------------------------------------------------------------- /app/frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 131 | 132 | 133 | 134 | 296 | 297 | 298 | 343 | -------------------------------------------------------------------------------- /app/frontend/src/assets/cloudflare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisdomSky/Cloudflared-web/878d8556a9b1fc52d99b07fa67130b9d8e985588/app/frontend/src/assets/cloudflare.png -------------------------------------------------------------------------------- /app/frontend/src/assets/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --muted: 210 40% 96.1%; 11 | --muted-foreground: 215.4 16.3% 46.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --border: 214.3 31.8% 91.4%; 20 | --input: 214.3 31.8% 91.4%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --accent: 210 40% 96.1%; 29 | --accent-foreground: 222.2 47.4% 11.2%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 222.2 84% 4.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 222.2 84% 4.9%; 41 | --foreground: 210 40% 98%; 42 | 43 | --muted: 217.2 32.6% 17.5%; 44 | --muted-foreground: 215 20.2% 65.1%; 45 | 46 | --popover: 222.2 84% 4.9%; 47 | --popover-foreground: 210 40% 98%; 48 | 49 | --card: 222.2 84% 4.9%; 50 | --card-foreground: 210 40% 98%; 51 | 52 | --border: 217.2 32.6% 17.5%; 53 | --input: 217.2 32.6% 17.5%; 54 | 55 | --primary: 210 40% 98%; 56 | --primary-foreground: 222.2 47.4% 11.2%; 57 | 58 | --secondary: 217.2 32.6% 17.5%; 59 | --secondary-foreground: 210 40% 98%; 60 | 61 | --accent: 217.2 32.6% 17.5%; 62 | --accent-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 212.7 26.8% 83.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | 78 | } 79 | } -------------------------------------------------------------------------------- /app/frontend/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/alert/Alert.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/alert/AlertDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/alert/AlertTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from 'class-variance-authority' 2 | 3 | export { default as Alert } from './Alert.vue' 4 | export { default as AlertTitle } from './AlertTitle.vue' 5 | export { default as AlertDescription } from './AlertDescription.vue' 6 | 7 | export const alertVariants = cva( 8 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-background text-foreground', 13 | destructive: 14 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: 'default', 19 | }, 20 | }, 21 | ) 22 | 23 | export type AlertVariants = VariantProps 24 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from 'class-variance-authority' 2 | 3 | export { default as Button } from './Button.vue' 4 | 5 | export const buttonVariants = cva( 6 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 11 | destructive: 12 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 13 | outline: 14 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 15 | secondary: 16 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 17 | ghost: 'hover:bg-accent hover:text-accent-foreground', 18 | link: 'text-primary underline-offset-4 hover:underline', 19 | }, 20 | size: { 21 | default: 'h-10 px-4 py-2', 22 | xs: 'h-7 rounded px-2', 23 | sm: 'h-9 rounded-md px-3', 24 | lg: 'h-11 rounded-md px-8', 25 | icon: 'h-10 w-10', 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | size: 'default', 31 | }, 32 | }, 33 | ) 34 | 35 | export type ButtonVariants = VariantProps 36 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue' 2 | export { default as CardHeader } from './CardHeader.vue' 3 | export { default as CardTitle } from './CardTitle.vue' 4 | export { default as CardDescription } from './CardDescription.vue' 5 | export { default as CardContent } from './CardContent.vue' 6 | export { default as CardFooter } from './CardFooter.vue' 7 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogScrollContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 60 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogTrigger } from './DialogTrigger.vue' 4 | export { default as DialogHeader } from './DialogHeader.vue' 5 | export { default as DialogTitle } from './DialogTitle.vue' 6 | export { default as DialogDescription } from './DialogDescription.vue' 7 | export { default as DialogContent } from './DialogContent.vue' 8 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 9 | export { default as DialogFooter } from './DialogFooter.vue' 10 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/hover-card/HoverCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/hover-card/HoverCardContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/hover-card/HoverCardTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/hover-card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HoverCard } from './HoverCard.vue' 2 | export { default as HoverCardTrigger } from './HoverCardTrigger.vue' 3 | export { default as HoverCardContent } from './HoverCardContent.vue' 4 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/Select.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectContent.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 54 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectItem.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 45 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectItemText.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectLabel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectScrollDownButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectScrollUpButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectSeparator.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectTrigger.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/SelectValue.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from './Select.vue' 2 | export { default as SelectContent } from './SelectContent.vue' 3 | export { default as SelectGroup } from './SelectGroup.vue' 4 | export { default as SelectItem } from './SelectItem.vue' 5 | export { default as SelectItemText } from './SelectItemText.vue' 6 | export { default as SelectLabel } from './SelectLabel.vue' 7 | export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' 8 | export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' 9 | export { default as SelectSeparator } from './SelectSeparator.vue' 10 | export { default as SelectTrigger } from './SelectTrigger.vue' 11 | export { default as SelectValue } from './SelectValue.vue' 12 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue' 2 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/sonner/Sonner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from './Sonner.vue' 2 | -------------------------------------------------------------------------------- /app/frontend/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | 3 | import en from '@/locales/en' 4 | import zhCN from '@/locales/zh-CN' 5 | import de from '@/locales/de' 6 | import esBR from '@/locales/es-BR' 7 | import ru from '@/locales/ru' 8 | import ja from '@/locales/ja' 9 | import fr from '@/locales/fr' 10 | 11 | const i18n = createI18n({ 12 | locale: localStorage.getItem('locale') || "en", 13 | fallbackLocale: 'en', 14 | messages: { 15 | 'en': en, 16 | 'zh-CN': zhCN, 17 | 'de': de, 18 | 'es-BR': esBR, 19 | 'ru': ru, 20 | 'ja': ja, 21 | 'fr': fr 22 | } 23 | }) 24 | 25 | export default i18n 26 | -------------------------------------------------------------------------------- /app/frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/frontend/src/locales/de.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'A simple UI to run Cloudflare Tunnel': 'Eine einfache Benutzeroberfläche zum Ausführen von Cloudflare Tunnel', 3 | 'Enter Tunnel Connector Token:': 'Tunnel-Connector-Token eingeben:', 4 | 'A new version is available!': 'Eine neue Version ist verfügbar!', 5 | 'updateDockerImage': 'Aktualisieren Sie den Docker-Image-Tag auf {version}, um die neueste Version zu verwenden.', 6 | 'Save': 'Speichern', 7 | 'Cloudflared is currently running': 'Cloudflared läuft derzeit', 8 | 'Cloudflared is not running': 'Cloudflared wird nicht ausgeführt', 9 | 'Stop': 'Stopp', 10 | 'Start': 'Start', 11 | 'You can also enter the entire command into the input like:': 'Sie können auch den gesamten Befehl in die Eingabe eingeben, wie:', 12 | 'The token will be automatically extracted from it.': 'Das Token wird automatisch daraus extrahiert.', 13 | 'Do not show again': 'Nicht mehr anzeigen', 14 | 'Tip': 'Tipp', 15 | 'Local Configuration': 'Lokale Konfiguration', 16 | 'Advanced Configuration': 'Erweiterte Konfiguration', 17 | 'For advanced Cloudflare Tunnel configuration. See the': 'Für die erweiterte Cloudflare Tunnel-Konfiguration. Siehe die', 18 | 'documentation': 'Dokumentation', 19 | 'for more information.': 'für weitere Informationen.', 20 | 'Clear': 'Löschen', 21 | 'Save Config': 'Konfiguration speichern', 22 | 'ZeroTrust Dashboard': 'ZeroTrust Dashboard', 23 | 'Create a Cloudflare Tunnel': 'Erstellen Sie einen Cloudflare-Tunnel', 24 | 'Github': 'Github', 25 | 'Help and support this project by sponsoring us on': 'Helfen und unterstützen Sie dieses Projekt, indem Sie uns sponsern auf', 26 | 'All sponsors will be listed and featured here in the future.': 'Alle Sponsoren werden hier in Zukunft aufgelistet und vorgestellt.' 27 | } 28 | -------------------------------------------------------------------------------- /app/frontend/src/locales/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'A simple UI to run Cloudflare Tunnel': 'A simple UI to run Cloudflare Tunnel', 3 | 'Enter Tunnel Connector Token:': 'Enter Tunnel Connector Token:', 4 | 'A new version is available!': 'A new version is available!', 5 | 'updateDockerImage': 'Update the docker image tag into {version} to use the latest version.', 6 | 'Save': 'Save', 7 | 'Cloudflared is currently running': 'Cloudflared is currently running', 8 | 'Cloudflared is not running': 'Cloudflared is not running', 9 | 'Stop': 'Stop', 10 | 'Start': 'Start', 11 | 'You can also enter the entire command into the input like:': 'Enter the entire command into the input like:', 12 | 'The token will be automatically extracted from it.': 'The token will be automatically extracted from it.', 13 | 'Do not show again': 'Do not show again', 14 | 'Tip': 'Tip', 15 | 'Local Configuration': 'Local Configuration', 16 | 'Advanced Configuration': 'Advanced Configuration', 17 | 'For advanced Cloudflare Tunnel configuration. See the': 'For advanced Cloudflare Tunnel configuration. See the', 18 | 'documentation': 'documentation', 19 | 'for more information.': 'for more information.', 20 | 'Clear': 'Clear', 21 | 'Save Config': 'Save Config', 22 | 'ZeroTrust Dashboard': 'ZeroTrust Dashboard', 23 | 'Create a Cloudflare Tunnel': 'Create a Cloudflare Tunnel', 24 | 'Github': 'Github', 25 | 'Help and support this project by sponsoring us on': 'Help and support this project by sponsoring us on', 26 | 'All sponsors will be listed and featured here in the future.': 'All sponsors will be listed and featured here in the future.' 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/frontend/src/locales/es-BR.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'A simple UI to run Cloudflare Tunnel': 'Uma interface simples para executar o Cloudflare Tunnel', 3 | 'Enter Tunnel Connector Token:': 'Digite o Token do Conector de Túnel:', 4 | 'A new version is available!': 'Uma nova versão está disponível!', 5 | 'updateDockerImage': 'Atualize a tag da imagem do docker para {version} para usar a versão mais recente.', 6 | 'Save': 'Salvar', 7 | 'Cloudflared is currently running': 'O Cloudflared está em execução no momento', 8 | 'Cloudflared is not running': 'O Cloudflared não está em execução', 9 | 'Stop': 'Parar', 10 | 'Start': 'Iniciar', 11 | 'You can also enter the entire command into the input like:': 'Você também pode digitar o comando inteiro na entrada, como:', 12 | 'The token will be automatically extracted from it.': 'O token será extraído automaticamente dele.', 13 | 'Do not show again': 'Não mostrar novamente', 14 | 'Tip': 'Dica', 15 | 'Local Configuration': 'Configuração Local', 16 | 'Advanced Configuration': 'Configuração Avançada', 17 | 'For advanced Cloudflare Tunnel configuration. See the': 'Para configuração avançada do Cloudflare Tunnel. Veja a', 18 | 'documentation': 'documentação', 19 | 'for more information.': 'para mais informações.', 20 | 'Clear': 'Limpar', 21 | 'Save Config': 'Salvar Configuração', 22 | 'ZeroTrust Dashboard': 'Painel ZeroTrust', 23 | 'Create a Cloudflare Tunnel': 'Criar um Cloudflare Tunnel', 24 | 'Github': 'Github', 25 | 'Help and support this project by sponsoring us on': 'Ajude e apoie este projeto patrocinando-nos no', 26 | 'All sponsors will be listed and featured here in the future.': 'Todos os patrocinadores serão listados e apresentados aqui no futuro.' 27 | } 28 | -------------------------------------------------------------------------------- /app/frontend/src/locales/fr.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'A simple UI to run Cloudflare Tunnel': 'Une interface simple pour exécuter Cloudflare Tunnel', 3 | 'Enter Tunnel Connector Token:': 'Entrez le jeton du connecteur de tunnel:', 4 | 'A new version is available!': 'Une nouvelle version est disponible !', 5 | 'updateDockerImage': 'Mettez à jour le tag de l\'image docker en {version} pour utiliser la dernière version.', 6 | 'Save': 'Enregistrer', 7 | 'Cloudflared is currently running': 'Cloudflared est en cours d\'exécution', 8 | 'Cloudflared is not running': 'Cloudflared n\'est pas en cours d\'exécution', 9 | 'Stop': 'Arrêter', 10 | 'Start': 'Démarrer', 11 | 'You can also enter the entire command into the input like:': 'Vous pouvez également entrer la commande entière dans l\'entrée comme:', 12 | 'The token will be automatically extracted from it.': 'Le jeton en sera automatiquement extrait.', 13 | 'Do not show again': 'Ne plus afficher', 14 | 'Tip': 'Astuce', 15 | 'Local Configuration': 'Configuration locale', 16 | 'Advanced Configuration': 'Configuration avancée', 17 | 'For advanced Cloudflare Tunnel configuration. See the': 'Pour la configuration avancée de Cloudflare Tunnel. Voir la', 18 | 'documentation': 'documentation', 19 | 'for more information.': 'pour plus d\'informations.', 20 | 'Clear': 'Effacer', 21 | 'Save Config': 'Enregistrer la configuration', 22 | 'ZeroTrust Dashboard': 'Tableau de bord ZeroTrust', 23 | 'Create a Cloudflare Tunnel': 'Créer un Cloudflare Tunnel', 24 | 'Github': 'Github', 25 | 'Help and support this project by sponsoring us on': 'Aidez et soutenez ce projet en nous sponsorisant sur', 26 | 'All sponsors will be listed and featured here in the future.': 'Tous les sponsors seront listés et présentés ici à l\'avenir.' 27 | } 28 | -------------------------------------------------------------------------------- /app/frontend/src/locales/ja.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'A simple UI to run Cloudflare Tunnel': 'Cloudflare Tunnelを実行するシンプルなUI', 3 | 'Enter Tunnel Connector Token:': 'トンネルコネクタートークンを入力してください:', 4 | 'A new version is available!': '新しいバージョンが利用可能です!', 5 | 'updateDockerImage': '最新バージョンを使用するには、Dockerイメージタグを{version}に更新してください。', 6 | 'Save': '保存', 7 | 'Cloudflared is currently running': 'Cloudflaredは現在実行中です', 8 | 'Cloudflared is not running': 'Cloudflaredは実行されていません', 9 | 'Stop': '停止', 10 | 'Start': '開始', 11 | 'You can also enter the entire command into the input like:': '次のように、コマンド全体を入力に入力することもできます:', 12 | 'The token will be automatically extracted from it.': 'トークンは自動的に抽出されます。', 13 | 'Do not show again': '今後表示しない', 14 | 'Tip': 'ヒント', 15 | 'Local Configuration': 'ローカル構成', 16 | 'Advanced Configuration': '高度な構成', 17 | 'For advanced Cloudflare Tunnel configuration. See the': '高度なCloudflare Tunnel構成については、', 18 | 'documentation': 'ドキュメント', 19 | 'for more information.': 'を参照してください。', 20 | 'Clear': 'クリア', 21 | 'Save Config': '構成を保存', 22 | 'ZeroTrust Dashboard': 'ZeroTrustダッシュボード', 23 | 'Create a Cloudflare Tunnel': 'Cloudflare Tunnelを作成', 24 | 'Github': 'Github', 25 | 'Help and support this project by sponsoring us on': 'このプロジェクトを支援してください', 26 | 'All sponsors will be listed and featured here in the future.': '将来的にはすべてのスポンサーがここに掲載され、紹介されます。' 27 | } 28 | -------------------------------------------------------------------------------- /app/frontend/src/locales/ru.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'A simple UI to run Cloudflare Tunnel': 'Простой интерфейс для запуска Cloudflare Tunnel', 3 | 'Enter Tunnel Connector Token:': 'Введите токен коннектора туннеля:', 4 | 'A new version is available!': 'Доступна новая версия!', 5 | 'updateDockerImage': 'Обновите тег образа docker на {version}, чтобы использовать последнюю версию.', 6 | 'Save': 'Сохранить', 7 | 'Cloudflared is currently running': 'Cloudflared в настоящее время запущен', 8 | 'Cloudflared is not running': 'Cloudflared не запущен', 9 | 'Stop': 'Остановить', 10 | 'Start': 'Запустить', 11 | 'You can also enter the entire command into the input like:': 'Вы также можете ввести всю команду в поле ввода, например:', 12 | 'The token will be automatically extracted from it.': 'Токен будет автоматически извлечен из него.', 13 | 'Do not show again': 'Больше не показывать', 14 | 'Tip': 'Совет', 15 | 'Local Configuration': 'Локальная конфигурация', 16 | 'Advanced Configuration': 'Расширенная конфигурация', 17 | 'For advanced Cloudflare Tunnel configuration. See the': 'Для расширенной конфигурации Cloudflare Tunnel. Смотрите', 18 | 'documentation': 'документацию', 19 | 'for more information.': 'для получения дополнительной информации.', 20 | 'Clear': 'Очистить', 21 | 'Save Config': 'Сохранить конфигурацию', 22 | 'ZeroTrust Dashboard': 'Панель управления ZeroTrust', 23 | 'Create a Cloudflare Tunnel': 'Создать Cloudflare Tunnel', 24 | 'Github': 'Github', 25 | 'Help and support this project by sponsoring us on': 'Помогите и поддержите этот проект, спонсируя нас на', 26 | 'All sponsors will be listed and featured here in the future.': 'Все спонсоры будут перечислены и представлены здесь в будущем.' 27 | } 28 | -------------------------------------------------------------------------------- /app/frontend/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'A simple UI to run Cloudflare Tunnel': '一个简单的用户界面来运行 Cloudflare Tunnel', 3 | 'Enter Tunnel Connector Token:': '输入 Tunnel 连接器令牌:', 4 | 'A new version is available!': '有新版本可用!', 5 | 'updateDockerImage': '将 Docker 镜像标签更新为 {version} 以使用最新版本。', 6 | 'Save': '保存', 7 | 'Cloudflared is currently running': 'Cloudflared 当前正在运行', 8 | 'Cloudflared is not running': 'Cloudflared 未在运行', 9 | 'Stop': '停止', 10 | 'Start': '启动', 11 | 'You can also enter the entire command into the input like:': '您也可以将整个命令输入到输入框中,如:', 12 | 'The token will be automatically extracted from it.': '令牌将从中自动提取。', 13 | 'Do not show again': '不再显示', 14 | 'Tip': '提示', 15 | 'Local Configuration': '本地配置', 16 | 'Advanced Configuration': '高级配置', 17 | 'For advanced Cloudflare Tunnel configuration. See the': '对于高级 Cloudflare Tunnel 配置。请参阅', 18 | 'documentation': '文档', 19 | 'for more information.': '以获取更多信息。', 20 | 'Clear': '清除', 21 | 'Save Config': '保存配置', 22 | 'ZeroTrust Dashboard': 'ZeroTrust 仪表板', 23 | 'Create a Cloudflare Tunnel': '创建 Cloudflare Tunnel', 24 | 'Github': 'Github', 25 | 'Help and support this project by sponsoring us on': '通过在 Github 上赞助我们来帮助和支持这个项目', 26 | 'All sponsors will be listed and featured here in the future.': '所有赞助商将在未来在此列出和特别介绍。' 27 | } 28 | -------------------------------------------------------------------------------- /app/frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './assets/index.css' 3 | import App from './App.vue' 4 | import i18n from './i18n' 5 | 6 | 7 | const app = createApp(App) 8 | app.use(i18n) 9 | app.mount('#app') 10 | -------------------------------------------------------------------------------- /app/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const animate = require("tailwindcss-animate") 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ["class"], 6 | safelist: ["dark"], 7 | prefix: "", 8 | 9 | content: [ 10 | './pages/**/*.{ts,tsx,vue}', 11 | './components/**/*.{ts,tsx,vue}', 12 | './app/**/*.{ts,tsx,vue}', 13 | './src/**/*.{ts,tsx,vue}', 14 | ], 15 | 16 | theme: { 17 | container: { 18 | center: true, 19 | padding: "2rem", 20 | screens: { 21 | "2xl": "1400px", 22 | }, 23 | }, 24 | extend: { 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | muted: { 44 | DEFAULT: "hsl(var(--muted))", 45 | foreground: "hsl(var(--muted-foreground))", 46 | }, 47 | accent: { 48 | DEFAULT: "hsl(var(--accent))", 49 | foreground: "hsl(var(--accent-foreground))", 50 | }, 51 | popover: { 52 | DEFAULT: "hsl(var(--popover))", 53 | foreground: "hsl(var(--popover-foreground))", 54 | }, 55 | card: { 56 | DEFAULT: "hsl(var(--card))", 57 | foreground: "hsl(var(--card-foreground))", 58 | }, 59 | }, 60 | borderRadius: { 61 | xl: "calc(var(--radius) + 4px)", 62 | lg: "var(--radius)", 63 | md: "calc(var(--radius) - 2px)", 64 | sm: "calc(var(--radius) - 4px)", 65 | }, 66 | keyframes: { 67 | "accordion-down": { 68 | from: { height: 0 }, 69 | to: { height: "var(--radix-accordion-content-height)" }, 70 | }, 71 | "accordion-up": { 72 | from: { height: "var(--radix-accordion-content-height)" }, 73 | to: { height: 0 }, 74 | }, 75 | "collapsible-down": { 76 | from: { height: 0 }, 77 | to: { height: 'var(--radix-collapsible-content-height)' }, 78 | }, 79 | "collapsible-up": { 80 | from: { height: 'var(--radix-collapsible-content-height)' }, 81 | to: { height: 0 }, 82 | }, 83 | }, 84 | animation: { 85 | "accordion-down": "accordion-down 0.2s ease-out", 86 | "accordion-up": "accordion-up 0.2s ease-out", 87 | "collapsible-down": "collapsible-down 0.2s ease-in-out", 88 | "collapsible-up": "collapsible-up 0.2s ease-in-out", 89 | }, 90 | }, 91 | }, 92 | plugins: [animate], 93 | } -------------------------------------------------------------------------------- /app/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /app/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "composite": true, 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /app/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | import tailwind from 'tailwindcss' 6 | import autoprefixer from 'autoprefixer' 7 | import topLevelAwait from "vite-plugin-top-level-await"; 8 | 9 | 10 | export default defineConfig({ 11 | css: { 12 | postcss: { 13 | plugins: [tailwind(), autoprefixer()], 14 | }, 15 | }, 16 | plugins: [ 17 | vue(), 18 | topLevelAwait({ 19 | // The export name of top-level await promise for each chunk module 20 | promiseExportName: "__tla", 21 | // The function to generate import names of top-level await promise in each chunk module 22 | promiseImportName: i => `__tla_${i}` 23 | }) 24 | ], 25 | resolve: { 26 | alias: { 27 | '@': path.resolve(__dirname, './src'), 28 | }, 29 | }, 30 | }) -------------------------------------------------------------------------------- /dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-bookworm-slim 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | ARG TARGETVARIANT 6 | 7 | ARG CLOUDFLARED_VERSION=2025.4.2 8 | ARG CLOUDFLARED_BASE_URL="https://github.com/cloudflare/cloudflared/releases/download" 9 | 10 | ENV VERSION=$CLOUDFLARED_VERSION 11 | ENV WEBUI_PORT=${WEBUI_PORT:-14333} 12 | ENV METRICS_ENABLE=${METRICS_ENABLE:-"false"} 13 | ENV METRICS_PORT=${METRICS_PORT:-60123} 14 | 15 | ENV EDGE_IP_VERSION=auto 16 | ENV PROTOCOL=auto 17 | 18 | EXPOSE ${WEBUI_PORT} 19 | EXPOSE ${METRICS_PORT} 20 | 21 | USER root 22 | WORKDIR /var/app 23 | 24 | RUN apt update && apt upgrade -y && apt install -y curl 25 | 26 | RUN if [ "$TARGETVARIANT" = "v7" ]; then \ 27 | CLOUDFLARED_PKG="cloudflared-$TARGETOS-${TARGETARCH}hf.deb"; \ 28 | else \ 29 | CLOUDFLARED_PKG="cloudflared-$TARGETOS-$TARGETARCH.deb"; \ 30 | fi && \ 31 | curl -L --output cloudflared.deb "$CLOUDFLARED_BASE_URL/$CLOUDFLARED_VERSION/$CLOUDFLARED_PKG" && \ 32 | dpkg -i cloudflared.deb && \ 33 | rm cloudflared.deb 34 | 35 | VOLUME /root/.cloudflared 36 | VOLUME /config 37 | 38 | VOLUME /var/app/backend 39 | VOLUME /var/app/frontend 40 | 41 | -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | # Running dev 2 | 3 | sh up.sh 4 | 5 | Once inside the the container, you have to manually build and run the app: 6 | 7 | cd /var/app/frontend && npm install && npm run build 8 | cd /var/app/backend && npm install 9 | node app.js 10 | 11 | During development, you can set the module bundler of the frontend app to watch for changes using `npm run watch`: 12 | 13 | cd /var/app/frontend && npm install && npm run watch 14 | -------------------------------------------------------------------------------- /dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | image: cloudflared-web 5 | container_name: cloudflared-web 6 | privileged: true 7 | network_mode: host 8 | environment: 9 | WEBUI_PORT: 1111 10 | METRICS_ENABLE: 'true' 11 | volumes: 12 | - ./config:/config 13 | - ../app/backend:/var/app/backend 14 | - ../app/frontend:/var/app/frontend 15 | - ./.cloudflared:/root/.cloudflared 16 | tty: true 17 | stdin_open: true 18 | -------------------------------------------------------------------------------- /dev/down.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo docker compose down 4 | sudo docker rmi -f cloudflared-web -------------------------------------------------------------------------------- /dev/up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo docker compose up -d 4 | sudo docker exec -ti cloudflared-web bash -------------------------------------------------------------------------------- /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisdomSky/Cloudflared-web/878d8556a9b1fc52d99b07fa67130b9d8e985588/screenshot-1.png -------------------------------------------------------------------------------- /screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisdomSky/Cloudflared-web/878d8556a9b1fc52d99b07fa67130b9d8e985588/screenshot-2.png --------------------------------------------------------------------------------