2023-12-05 05:17:36 +00:00
|
|
|
import * as fs from "fs"
|
|
|
|
|
import {dirname} from "path"
|
|
|
|
|
import {spawnSync} from "child_process"
|
|
|
|
|
import {X509Certificate} from "crypto"
|
|
|
|
|
|
2023-12-18 04:14:31 +00:00
|
|
|
// The custom domain is expected to only have the domain. So if it has a protocol, we ignore the whole value.
|
|
|
|
|
// This was the effective behaviour before Caddy.
|
|
|
|
|
const CUSTOM_DOMAIN = (process.env.APPSMITH_CUSTOM_DOMAIN || "").replace(/^https?:\/\/.+$/, "")
|
2023-12-05 05:17:36 +00:00
|
|
|
const CaddyfilePath = process.env.TMP + "/Caddyfile"
|
2024-05-24 07:41:56 +00:00
|
|
|
const AppsmithCaddy = process.env._APPSMITH_CADDY
|
|
|
|
|
|
|
|
|
|
// Rate limit environment.
|
|
|
|
|
const isRateLimitingEnabled = process.env.APPSMITH_RATE_LIMIT !== "disabled"
|
|
|
|
|
const RATE_LIMIT = parseInt(process.env.APPSMITH_RATE_LIMIT || 100, 10)
|
2023-12-05 05:17:36 +00:00
|
|
|
|
|
|
|
|
let certLocation = null
|
2023-12-18 04:14:31 +00:00
|
|
|
if (CUSTOM_DOMAIN !== "") {
|
2023-12-05 05:17:36 +00:00
|
|
|
try {
|
|
|
|
|
fs.accessSync("/appsmith-stacks/ssl/fullchain.pem", fs.constants.R_OK)
|
|
|
|
|
certLocation = "/appsmith-stacks/ssl"
|
2023-12-18 04:14:31 +00:00
|
|
|
} catch {
|
2023-12-05 05:17:36 +00:00
|
|
|
// no custom certs, see if old certbot certs are there.
|
2024-01-11 04:28:52 +00:00
|
|
|
const letsEncryptCertLocation = "/appsmith-stacks/letsencrypt/live/" + CUSTOM_DOMAIN
|
2023-12-05 05:17:36 +00:00
|
|
|
const fullChainPath = letsEncryptCertLocation + `/fullchain.pem`
|
|
|
|
|
try {
|
|
|
|
|
fs.accessSync(fullChainPath, fs.constants.R_OK)
|
|
|
|
|
console.log("Old Let's Encrypt cert file exists, now checking if it's expired.")
|
|
|
|
|
if (!isCertExpired(fullChainPath)) {
|
|
|
|
|
certLocation = letsEncryptCertLocation
|
|
|
|
|
}
|
2023-12-18 04:14:31 +00:00
|
|
|
} catch {
|
2023-12-05 05:17:36 +00:00
|
|
|
// no certs there either, ignore.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-11 13:55:12 +00:00
|
|
|
const frameAncestorsPolicy = (process.env.APPSMITH_ALLOWED_FRAME_ANCESTORS || "'self'")
|
|
|
|
|
.replace(/;.*$/, "")
|
|
|
|
|
|
2023-12-05 05:17:36 +00:00
|
|
|
const parts = []
|
|
|
|
|
|
|
|
|
|
parts.push(`
|
|
|
|
|
{
|
2024-09-13 10:00:08 +00:00
|
|
|
admin 0.0.0.0:2019
|
2023-12-05 05:17:36 +00:00
|
|
|
persist_config off
|
|
|
|
|
acme_ca_root /etc/ssl/certs/ca-certificates.crt
|
|
|
|
|
servers {
|
2025-04-03 14:09:23 +00:00
|
|
|
protocols h1 h2 h3
|
2023-12-05 05:17:36 +00:00
|
|
|
trusted_proxies static 0.0.0.0/0
|
2024-09-17 13:29:51 +00:00
|
|
|
metrics
|
2023-12-05 05:17:36 +00:00
|
|
|
}
|
2024-05-24 07:41:56 +00:00
|
|
|
${isRateLimitingEnabled ? "order rate_limit before basicauth" : ""}
|
2023-12-05 05:17:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(file_server) {
|
|
|
|
|
file_server {
|
|
|
|
|
precompressed br gzip
|
|
|
|
|
disable_canonical_uris
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(reverse_proxy) {
|
|
|
|
|
reverse_proxy {
|
|
|
|
|
to 127.0.0.1:{args[0]}
|
|
|
|
|
header_up -Forwarded
|
2024-05-02 07:30:23 +00:00
|
|
|
header_up X-Appsmith-Request-Id {http.request.uuid}
|
2023-12-05 05:17:36 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(all-config) {
|
|
|
|
|
log {
|
|
|
|
|
output stdout
|
|
|
|
|
}
|
2024-09-25 05:31:18 +00:00
|
|
|
|
|
|
|
|
# skip logs for health check
|
2024-10-07 08:29:29 +00:00
|
|
|
log_skip /api/v1/health
|
2023-12-05 05:17:36 +00:00
|
|
|
|
2024-09-25 05:31:18 +00:00
|
|
|
# skip logs for sourcemap files
|
|
|
|
|
@source-map-files {
|
|
|
|
|
path_regexp ^.*\.(js|css)\.map$
|
|
|
|
|
}
|
2024-10-07 08:29:29 +00:00
|
|
|
log_skip @source-map-files
|
2024-09-25 05:31:18 +00:00
|
|
|
|
2024-05-02 07:30:23 +00:00
|
|
|
# The internal request ID header should never be accepted from an incoming request.
|
|
|
|
|
request_header -X-Appsmith-Request-Id
|
|
|
|
|
|
|
|
|
|
# Ref: https://stackoverflow.com/a/38191078/151048
|
|
|
|
|
# We're only accepting v4 UUIDs today, in order to not make it too lax unless needed.
|
|
|
|
|
@valid-request-id expression {header.X-Request-Id}.matches("(?i)^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$")
|
|
|
|
|
header @valid-request-id X-Request-Id {header.X-Request-Id}
|
|
|
|
|
@invalid-request-id expression !{header.X-Request-Id}.matches("(?i)^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$")
|
|
|
|
|
header @invalid-request-id X-Request-Id invalid_request_id
|
|
|
|
|
request_header @invalid-request-id X-Request-Id invalid_request_id
|
|
|
|
|
|
2023-12-05 05:17:36 +00:00
|
|
|
header {
|
|
|
|
|
-Server
|
2023-12-11 13:55:12 +00:00
|
|
|
Content-Security-Policy "frame-ancestors ${frameAncestorsPolicy}"
|
2023-12-05 05:17:36 +00:00
|
|
|
X-Content-Type-Options "nosniff"
|
2024-05-02 07:30:23 +00:00
|
|
|
X-Appsmith-Request-Id {http.request.uuid}
|
2023-12-05 05:17:36 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-23 07:15:13 +00:00
|
|
|
header /static/* {
|
|
|
|
|
Cache-Control "public, max-age=31536000, immutable"
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-05 05:17:36 +00:00
|
|
|
request_body {
|
2024-03-04 10:40:07 +00:00
|
|
|
max_size ${process.env.APPSMITH_CODEC_SIZE || 150}MB
|
2023-12-05 05:17:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handle {
|
|
|
|
|
root * {$WWW_PATH}
|
|
|
|
|
try_files /loading.html /index.html
|
|
|
|
|
import file_server
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
root * /opt/appsmith/editor
|
|
|
|
|
@file file
|
|
|
|
|
handle @file {
|
|
|
|
|
import file_server
|
|
|
|
|
}
|
fix: Incorrect status code for missing static files (#29374)
I think the route precedence in Caddy is different when using `handle`
directive, vs when directly using the `error` directive.
This is causing the file `handle {` route, which is a catch-all route is
handling `/static/*` requests that don't have a corresponding file. This
handler however, doesn't respond with 404 status, it responds with 200
status for missing files, and render the `index.html` for our SPA
behaviour.
Now, the CDN we have on release.app.appsmith.com caches responses from
upstream when the status is 200. If it is 404, it won't cache and retry
next time. This is why it's essential that we respond with 404 for files
that don't exist, irrespective of the content of the response.
When the container is starting up, Caddy doesn't have all the
information yet, and may have responded with not-found for one of the
assets. But since this went out with 200 status, our CDN cached it, and
once the file _was_ available with Caddy, the CDN wouldn't retry ever.
This fix will ensure we get 404 status code for requests to `/static/*`
that point to files that don't exist.
2023-12-06 09:12:25 +00:00
|
|
|
|
|
|
|
|
handle /static/* {
|
|
|
|
|
error 404
|
|
|
|
|
}
|
2023-12-05 05:17:36 +00:00
|
|
|
|
|
|
|
|
handle /info {
|
|
|
|
|
root * /opt/appsmith
|
|
|
|
|
rewrite * /info.json
|
|
|
|
|
import file_server
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@backend path /api/* /oauth2/* /login/*
|
|
|
|
|
handle @backend {
|
|
|
|
|
import reverse_proxy 8080
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handle /rts/* {
|
|
|
|
|
import reverse_proxy 8091
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
redir /supervisor /supervisor/
|
|
|
|
|
handle_path /supervisor/* {
|
|
|
|
|
import reverse_proxy 9001
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-24 07:41:56 +00:00
|
|
|
${isRateLimitingEnabled ? `rate_limit {
|
2024-03-07 10:52:29 +00:00
|
|
|
zone dynamic_zone {
|
2024-11-20 05:10:02 +00:00
|
|
|
# This key is designed to work irrespective of any load balancers running on the Appsmith container.
|
|
|
|
|
# We use "+" as the separator here since we don't expect it in any of the placeholder values here, and has no
|
|
|
|
|
# significance in header value syntax.
|
|
|
|
|
key {header.Forwarded}+{header.X-Forwarded-For}+{remote_host}
|
2024-03-07 10:52:29 +00:00
|
|
|
events ${RATE_LIMIT}
|
|
|
|
|
window 1s
|
|
|
|
|
}
|
2024-05-24 07:41:56 +00:00
|
|
|
}`: ""}
|
2024-03-07 10:52:29 +00:00
|
|
|
|
2023-12-05 05:17:36 +00:00
|
|
|
handle_errors {
|
|
|
|
|
respond "{err.status_code} {err.status_text}" {err.status_code}
|
2024-10-07 08:29:29 +00:00
|
|
|
header {
|
|
|
|
|
# Remove the Server header from the response.
|
|
|
|
|
-Server
|
|
|
|
|
# Remove Cache-Control header from the response.
|
|
|
|
|
-Cache-Control
|
|
|
|
|
}
|
2023-12-05 05:17:36 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-18 04:14:31 +00:00
|
|
|
# We bind to http on 80, so that localhost requests don't get redirected to https.
|
2024-05-21 06:37:58 +00:00
|
|
|
:${process.env.PORT || 80} {
|
2023-12-05 05:17:36 +00:00
|
|
|
import all-config
|
|
|
|
|
}
|
2023-12-18 04:14:31 +00:00
|
|
|
`)
|
2023-12-05 05:17:36 +00:00
|
|
|
|
2023-12-18 04:14:31 +00:00
|
|
|
if (CUSTOM_DOMAIN !== "") {
|
2024-01-24 09:49:17 +00:00
|
|
|
if (certLocation) {
|
|
|
|
|
// There's a custom certificate, don't bind to any exact domain.
|
|
|
|
|
parts.push(`
|
|
|
|
|
https:// {
|
|
|
|
|
import all-config
|
|
|
|
|
tls ${certLocation}/fullchain.pem ${certLocation}/privkey.pem
|
|
|
|
|
}
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// No custom certificate, bind to the custom domain explicitly, so Caddy can auto-provision the cert.
|
|
|
|
|
parts.push(`
|
|
|
|
|
https://${CUSTOM_DOMAIN} {
|
|
|
|
|
import all-config
|
|
|
|
|
}
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-18 04:14:31 +00:00
|
|
|
// We have to own the http-to-https redirect, since we need to remove the `Server` header from the response.
|
|
|
|
|
parts.push(`
|
|
|
|
|
http://${CUSTOM_DOMAIN} {
|
|
|
|
|
redir https://{host}{uri}
|
|
|
|
|
header -Server
|
|
|
|
|
header Connection close
|
|
|
|
|
}
|
|
|
|
|
`)
|
2023-12-05 05:17:36 +00:00
|
|
|
}
|
|
|
|
|
|
2024-04-19 01:08:01 +00:00
|
|
|
if (!process.argv.includes("--no-finalize-index-html")) {
|
2024-11-26 06:11:01 +00:00
|
|
|
finalizeHtmlFiles()
|
2024-04-19 01:08:01 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-05 05:17:36 +00:00
|
|
|
fs.mkdirSync(dirname(CaddyfilePath), { recursive: true })
|
|
|
|
|
fs.writeFileSync(CaddyfilePath, parts.join("\n"))
|
2024-05-24 07:41:56 +00:00
|
|
|
spawnSync(AppsmithCaddy, ["fmt", "--overwrite", CaddyfilePath])
|
|
|
|
|
spawnSync(AppsmithCaddy, ["reload", "--config", CaddyfilePath])
|
2023-12-05 05:17:36 +00:00
|
|
|
|
2024-11-26 06:11:01 +00:00
|
|
|
function finalizeHtmlFiles() {
|
2024-02-27 10:37:57 +00:00
|
|
|
let info = null;
|
|
|
|
|
try {
|
|
|
|
|
info = JSON.parse(fs.readFileSync("/opt/appsmith/info.json", "utf8"))
|
|
|
|
|
} catch(e) {
|
|
|
|
|
// info will be empty, that's okay.
|
2024-04-19 01:08:01 +00:00
|
|
|
console.error("Error reading info.json", e.message)
|
2024-02-27 10:37:57 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-31 06:08:49 +00:00
|
|
|
const extraEnv = {
|
2024-02-27 10:37:57 +00:00
|
|
|
APPSMITH_VERSION_ID: info?.version ?? "",
|
|
|
|
|
APPSMITH_VERSION_SHA: info?.commitSha ?? "",
|
|
|
|
|
APPSMITH_VERSION_RELEASE_DATE: info?.imageBuiltAt ?? "",
|
2024-12-26 05:07:41 +00:00
|
|
|
APPSMITH_HOSTNAME: process.env.HOSTNAME ?? "appsmith-0"
|
2024-01-31 06:08:49 +00:00
|
|
|
}
|
|
|
|
|
|
2024-11-26 06:11:01 +00:00
|
|
|
for (const file of ["index.html", "404.html"]) {
|
|
|
|
|
const content = fs.readFileSync("/opt/appsmith/editor/" + file, "utf8").replaceAll(
|
|
|
|
|
/\{\{env\s+"(APPSMITH_[A-Z0-9_]+)"}}/g,
|
|
|
|
|
(_, name) => (process.env[name] || extraEnv[name] || "")
|
|
|
|
|
)
|
2024-01-31 06:08:49 +00:00
|
|
|
|
2024-11-26 06:11:01 +00:00
|
|
|
fs.writeFileSync(process.env.WWW_PATH + "/" + file, content)
|
|
|
|
|
}
|
2024-01-31 06:08:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-05 05:17:36 +00:00
|
|
|
function isCertExpired(path) {
|
|
|
|
|
const cert = new X509Certificate(fs.readFileSync(path, "utf-8"))
|
|
|
|
|
console.log(path, cert)
|
|
|
|
|
return new Date(cert.validTo) < new Date()
|
|
|
|
|
}
|