diff --git a/client/Dockerfile b/client/Dockerfile index ea5151fa..a5a37c59 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -17,12 +17,13 @@ COPY docker-start.sh / WORKDIR /etc/nginx COPY nginx.conf.docker ./nginx.conf +COPY metatags.lua . WORKDIR /var/www COPY --from=builder /opt/app/public/ . -FROM nginx:alpine as release +FROM openresty/openresty:alpine as release RUN apk --no-cache add dumb-init COPY --from=approot / / diff --git a/client/docker-start.sh b/client/docker-start.sh index 0b2bec8a..bca05fe2 100755 --- a/client/docker-start.sh +++ b/client/docker-start.sh @@ -8,4 +8,5 @@ sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \ /var/www/manifest.json # Start server -exec nginx +echo "$0 starting server..." +exec openresty -c /etc/nginx/nginx.conf diff --git a/client/html/index.htm b/client/html/index.htm index 2f0f4e40..98bf63d2 100644 --- a/client/html/index.htm +++ b/client/html/index.htm @@ -22,6 +22,7 @@ + {{ generated_head_tags }}
diff --git a/client/metatags.lua b/client/metatags.lua new file mode 100644 index 00000000..bebd51e1 --- /dev/null +++ b/client/metatags.lua @@ -0,0 +1,135 @@ +-- Set the response content type back to HTML +ngx.header.content_type = 'text/html'; + +ngx.req.read_body() + +local page_html = ngx.location.capture("/index.htm").body + +local before_content, placeholder, after_content +before_content, placeholder, after_content = page_html:match( + string.format("^(.*)(%s)(.*)$", html_head_tag_replacement_str) +) + +-- check for placeholder to replace +if not placeholder then + ngx.log(ngx.STDERR, "WARNING: Meta tag placeholder was not found in page html.") + ngx.say(page_html) + return +else + -- start the response + ngx.print(before_content) + -- send partial content to the client to allow preloading the app + ngx.flush() +end + +ngx.req.set_header("Accept", "application/json") +local server_info_resp = ngx.location.capture("/_internal_api/info") +if server_info_resp.status ~= 200 then + ngx.log(ngx.STDERR, "Failed to acquire server info from szurubooru API, unable to generate meta tags: HTTP status "..server_info_resp.status) + ngx.print(after_content) + return +end +local server_info = cjson.decode(server_info_resp.body) + +local additional_tags = "" +local function add_meta_tag (property, content) + -- NOTE do not allow user-provided data in the property name, only the content has quotes escaped + additional_tags = additional_tags .. "" +end + +-- Add the site name tag +add_meta_tag("og:site_name", server_info.config.name) +add_meta_tag("og:url", ngx.var.external_host_url .. ngx.var.request_uri_path) + +local function generate_meta_tags () + if ngx.var.request_uri_path:match('^/$') then -- Site root + add_meta_tag("og:type", "website") + add_meta_tag("og:title", server_info.config.name) + add_meta_tag("twitter:title", server_info.config.name) + -- if there's a featured post, let's use that as the image + if server_info.featuredPost then + local post_info = server_info.featuredPost + -- NOTE this is different from the normal Post tags, + -- notably we avoid setting the article type, author, time, etc + local og_media_prefix + if post_info.type == "image" then + og_media_prefix = "og:image" + add_meta_tag("twitter:card", "summary_large_image") + add_meta_tag("twitter:image", ngx.var.external_host_url .. '/' .. post_info.contentUrl) + elseif post_info.type == "video" then + og_media_prefix = "og:video" + -- some sites don't preview video, so at least provide a thumbnail + add_meta_tag("og:image", ngx.var.external_host_url .. '/' .. post_info.thumbnailUrl) + end + add_meta_tag(og_media_prefix..":url", ngx.var.external_host_url .. '/' .. post_info.contentUrl) + add_meta_tag(og_media_prefix..":width", post_info.canvasWidth) + add_meta_tag(og_media_prefix..":height", post_info.canvasHeight) + end + elseif ngx.var.request_uri_path:match('^/post/([0-9]+)') then -- Post metadata + -- check if posts are accessible to anonymous users: + if server_info.config.privileges["posts:view"] == "anonymous" then + local match, err = ngx.re.match(ngx.var.request_uri_path, "^/post/(?[0-9]+)") + local post_id = match["post_id"] + add_meta_tag("og:type", "article") + local post_info = cjson.decode((ngx.location.capture("/_internal_api/post/"..post_id)).body) + add_meta_tag("og:title", server_info.config.name .. " - Post " .. post_info.id) + add_meta_tag("twitter:title", server_info.config.name .. " - Post " .. post_info.id) + add_meta_tag("article:published_time", post_info.creationTime) + local og_media_prefix + if post_info.type == "video" then + og_media_prefix = "og:video" + add_meta_tag("twitter:card", "player") + add_meta_tag("twitter:player:width", post_info.canvasWidth) + add_meta_tag("twitter:player:height", post_info.canvasHeight) + -- some sites don't preview video, so at least provide a thumbnail + add_meta_tag("og:image", ngx.var.external_host_url .. '/' .. post_info.thumbnailUrl) + else + og_media_prefix = "og:image" + add_meta_tag("twitter:card", "summary_large_image") + add_meta_tag("twitter:image", ngx.var.external_host_url .. '/' .. post_info.contentUrl) + end + add_meta_tag(og_media_prefix..":url", ngx.var.external_host_url .. '/' .. post_info.contentUrl) + add_meta_tag(og_media_prefix..":width", post_info.canvasWidth) + add_meta_tag(og_media_prefix..":height", post_info.canvasHeight) + -- user is not present for anonymous uploads: + if post_info.user then + add_meta_tag("article:author", post_info.user.name) + end + else + -- no permission to retrieve post data + add_meta_tag("og:title", server_info.config.name .. " - Login required") + end + elseif ngx.var.request_uri_path:match('^/user/([^/]+)/?$') then -- User metadata + local username = ngx.var.request_uri_path:match('^/user/([^/]+)/?$') + -- check for permission to access user profiles + if server_info.config.privileges["users:view"] == "anonymous" then + local user_info_request = ngx.location.capture("/_internal_api/user/"..username) + add_meta_tag("og:type", "profile") + if user_info_request.status == 200 then + add_meta_tag("og:title", server_info.config.name .. " - " .. username) + local user_info = cjson.decode(user_info_request.body) + add_meta_tag("profile:username", user_info.name) + local avatar_url = user_info.avatarUrl + if avatar_url:match("^https?://") then + add_meta_tag("og:image", avatar_url) + else + add_meta_tag("og:image", ngx.var.external_host_url .. '/' .. avatar_url) + end + else + -- could not retrieve the user profile, show some defaults + add_meta_tag("og:title", server_info.config.name .. " - User not found") + end + else + -- no permission to view user data + end + end +end + +local status, err = pcall(generate_meta_tags) +if not status then + ngx.log(ngx.STDERR, "Failed to generate meta tags: "..tostring(err)) +end + +-- Once tags have been generated, write them and then finish the response +ngx.print(additional_tags) +ngx.print(after_content) diff --git a/client/nginx.conf.docker b/client/nginx.conf.docker index 98c18b35..b4c63e7e 100644 --- a/client/nginx.conf.docker +++ b/client/nginx.conf.docker @@ -1,5 +1,7 @@ worker_processes 1; -user nginx; +user nobody; + +pcre_jit on; error_log /dev/stderr warn; pid /var/run/nginx.pid; @@ -9,7 +11,7 @@ events { } http { - include /etc/nginx/mime.types; + include /usr/local/openresty/nginx/conf/mime.types; default_type application/octet-stream; log_format main '$remote_addr -> $request [$status] - ' @@ -23,6 +25,15 @@ http { server __BACKEND__:6666; } + map $request_uri $request_uri_path { + "~^(?P[^?]*)(\?.*)?$" $path; + } + + init_by_lua_block { + cjson = require("cjson") + html_head_tag_replacement_str = "{{ generated_head_tags }}" + } + server { listen 80 default_server; @@ -50,6 +61,9 @@ http { gzip_proxied expired no-cache no-store private auth; gzip_types text/plain application/json; + proxy_connect_timeout 180s; + proxy_send_timeout 300s; + proxy_read_timeout 600s; if ($request_uri ~* "/api/(.*)") { proxy_pass http://backend/$1; } @@ -71,7 +85,7 @@ http { location / { root /var/www; - try_files $uri /index.htm; + try_files $uri /_meta_tags_html; sendfile on; tcp_nopush on; @@ -81,6 +95,30 @@ http { gzip_proxied expired no-cache no-store private auth; } + location ~ ^/_internal_api/(.*)$ { + internal; + tcp_nodelay on; + + add_header 'Access-Control-Allow-Origin' '*'; + + gzip off; + proxy_connect_timeout 10s; + proxy_send_timeout 10s; + proxy_read_timeout 10s; + proxy_pass http://backend/$1; + } + + location /_meta_tags_html { + internal; + set $original_scheme $scheme; + if ( $http_x_forwarded_proto = 'https' ) { + set $original_scheme "https"; + } + root /var/www; + set $external_host_url "${original_scheme}://${http_host}"; + content_by_lua_file /etc/nginx/metatags.lua; + } + location @unauthorized { return 403 "Unauthorized"; default_type text/plain;