Code trang Mediumz trong 5 phút với chưa đến 100 dòng code

Hiện tại trang medium.com khá là khó truy cập ở Việt Nam, mọi người muốn truy cập hầu như đều phải dùng đến VPN.

Truy cập medium.com bị lỗi

Cũng không rõ nguyên nhân là gì, nhưng mình nghĩ là có thể làm 1 tool để proxy request đến Medium và trả về nội dung để có thể xem được. Ý tưởng này không phải của mình, trước đó có 1 site là medium0.com đã làm rồi, mình cũng sử dụng được 1 thời gian, nhưng gần đây thì không dùng được nữa, không biết là do chi phí duy trì hay vấn đề gì nữa. Do đó, mình sẽ tự làm 1 cái để dùng cũng như chia sẻ với bạn bè. Nếu có vấn đề về bản quyền hay bất cứ vấn đề gì khác thì mình sẽ đóng lại, chỉ để dùng một mình thôi 😀

Trước khi bắt tay vào code thì cùng ngâm cứu xem để làm được tool này thì mình cần xử lý những vấn đề gì. Luồng tổng quan sẽ như thế này:

Luồng tổng quan

Tiếp theo là đi xem thử 1 bài xem nội dung Medium đang trả ra thế nào. Mình thử với bài này: https://medium.com/upday-devs/kubernetes-on-a-high-traffic-environment-3-key-takeaways-39d3852fb515. À từ đoạn này phải bật VPN lên nhé.

Xem qua source và test thử thì thấy Medium render từ server rồi, mấy cái JS kia không cần load cũng được luôn. Vậy công việc của mình là chỉ cần lấy HTML này và trả về cho user luôn, không phải xử lý gì thêm.

Cách để proxy requests thì mình dùng cách giống với đã làm ở phần Custom domain cho Uptimerobot, nginx muôn năm.

Để cấu hình nginx proxy request đến medium.com thì cũng khá đơn giản, chỉ cần cấu hình proxy_passproxy_ssl_server_name on;

server {
    listen       80 default_server;
    server_name  _;

    location / {
        proxy_pass https://medium.com;
        proxy_ssl_server_name on;
    }
}

Do mình chạy Nginx bằng docker container và chỉ có 1 server block nên không cần phải cấu hình server_name. Ngoài ra, mình có sử dụng OrbStack để chạy docker trên MacOS nên chỉ cần chạy nginx container với tên là mediumz thì có thể truy cập qua domain: https://mediumz.orb.local/.

Thử ngay với bài ở trên: https://mediumz.orb.local/upday-devs/kubernetes-on-a-high-traffic-environment-3-key-takeaways-39d3852fb515

Tada, được luôn rồi nè. Nhưng chớ vội mừng, phải check thêm xem mấy cái ảnh trên trang là dùng domain nào, nếu tắt VPN đi thì có xem được không.

Inspect lên thì thấy Medium dùng domain miro.medium.com cho link ảnh. Thử tắt VPN đi xem thử và kết quả là hẹo luôn.

Link ảnh sẽ bị lỗi tương tự với link medium.com

Để proxy được cả link ảnh thì sẽ cần làm 2 việc:

  • Khi trả ra HTML thì thay thế tất cả link miro.medium.com thành miro.mediumz.orb.local
  • Update nginx để proxy request đến miro.medium.com

Nginx config được sửa thành

server {
    listen       80 default_server;
    server_name  _;

    location / {
        proxy_pass https://medium.com;
        proxy_ssl_server_name on;
        proxy_set_header Accept-Encoding "";
        sub_filter 'miro.medium.com' 'miro.mediumz.orb.local';
        sub_filter_once off;
    }
}

server {
    listen       80;
    server_name  miro.mediumz.orb.local;

    location / {
        proxy_pass https://miro.medium.com;
        proxy_ssl_server_name on;
    }
}

Mình dùng hàm sub_filter của module ngx_http_sub_module sẽ thay thế tất cả (sub_filter_once off;) miro.medium.com thành miro.mediumz.orb.local. Chú ý mình có set thêm Accept-Encoding ""; vào proxy request để response trả về từ medium.com là dạng plain text thì mới replace được. Nói thêm 1 chút về Accept-Encoding, Nginx sẽ forward phần lớn header (bao gồm Accept-Encoding) và đa số các trình duyệt đều truyền Accept-Encoding: gzip, deflate, br, khi đó response sẽ được nén giúp giảm dung lượng truyền tải. Trong trường hợp này mình đang cần sửa response, nên set Accept-Encoding ""; giúp mình nhận về plain text response để sửa sau đó.

Còn proxy request đến miro.medium.com thì đơn giản rồi, mình không giải thích thêm.

Kết quả sau khi sửa config, link ảnh đã được thay thế bằng link proxy, đảm bảo sẽ tải được ảnh mà không cần VPN.

Đến đây là xong mất tiêu rồi, mỗi tội là chưa đủ 100 dòng code. Thôi bonus thêm Dockerfile để build image nginx kèm ngx_http_sub_module vậy:

# Dockerfile
FROM nginx:stable as builder

WORKDIR /tmp
RUN export nginx_version=$(nginx -v 2>&1 | awk '{split($0, a); print a[3]}' | awk '{split($0, a, "/"); print a[2]}') && \
    apt-get update && \
        apt-get install -y curl gnupg2 ca-certificates lsb-release git gcc libpcre3 libpcre3-dev zlib1g zlib1g-dev build-essential && \
    echo "deb http://nginx.org/packages/debian `lsb_release -cs` nginx" \
        | tee /etc/apt/sources.list.d/nginx.list && \
    curl -fsSL https://nginx.org/keys/nginx_signing.key | apt-key add - && \
    git clone https://github.com/yaoweibin/ngx_http_substitutions_filter_module.git && \
    curl https://nginx.org/download/nginx-$nginx_version.tar.gz -o nginx.tar.gz && \
    echo https://nginx.org/download/nginx-$nginx_version.tar.gz && \
        tar -xzvf nginx.tar.gz && \
    cd nginx-$nginx_version && ./configure --with-compat --add-dynamic-module=../ngx_http_substitutions_filter_module && make modules && \
    cp /tmp/nginx-$nginx_version/objs/ngx_http_subs_filter_module.so /tmp/ngx_http_subs_filter_module.so

FROM nginx:stable-alpine as runner

COPY --from=builder /tmp/ngx_http_subs_filter_module.so /etc/nginx/modules/

COPY templates /etc/nginx/templates

Và cuối cùng vẫn chưa được 100 dòng code 😀

Code mình có để ở trên github, mọi người cùng tham khảo. Tuy nhiên đây chỉ là source code demo cho bài viết này thôi nhé.