顯示具有 proxy 標籤的文章。 顯示所有文章
顯示具有 proxy 標籤的文章。 顯示所有文章

2025年4月22日 星期二

Python 開發筆記 - 使用 http.server 建置嵌入式產品網頁開發環境與 Proxy 機制

這個需求快十年前弄過,那時是用 webpack proxy server,可以在習慣的桌機開發網頁,接著對於 CGI 查詢就透過 proxy 導向到 device CGI 

然後,現在弄一個 Python ,方便後續在老 server 上運行,畢竟肥肥的程式碼搭配檔案系統大小寫問題,還是交給 server 檔案系統處理吧,如此,未來有部分 html/js/css code 要快速測試,就只需要到指定機器上,運行

$ python3 proxy-server.py --port 12345 --proxy-target 'http://192.168.123.234:80' --proxy-paths '/cgi-bin/' -d project/path/html/code/
Serving files from directory: project/path/html/code/
Serving HTTP on 0.0.0.0 port 12345 (multi-threaded) ...
Proxying paths ['/cgi-bin/'] to http://192.168.123.234:80
^C
Server stopped.

如此對於 CGI request 就會自動導向到 'http://192.168.123.234:80' 

程式碼純筆記,這都是 AI 產的:

#!/usr/bin/env python3

import http.server
import socketserver
import http.client
import argparse
import os
import sys
import socket
from urllib.parse import urlparse

# 使用 ThreadingMixIn 來處理並發請求
class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    daemon_threads = True
    allow_reuse_address = True


class ProxyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    # 代理目標地址
    proxy_target = None
    # 需要代理的路徑前綴
    proxy_paths = []
    # 連接池(簡單實現)
    conn_pool = {}
    
    def __init__(self, *args, **kwargs):
        # 確保可以正確處理目錄參數
        super().__init__(*args, **kwargs)
    
    def do_GET(self):
        # 檢查是否需要代理這個請求
        for path_prefix in self.proxy_paths:
            if self.path.startswith(path_prefix):
                self.proxy_request()
                return
        
        # 如果不需要代理,則使用默認處理方式
        super().do_GET()
    
    def do_POST(self):
        # 為 POST 請求也提供代理功能
        for path_prefix in self.proxy_paths:
            if self.path.startswith(path_prefix):
                self.proxy_request()
                return
        
        super().do_POST()
    
    def get_connection(self, host, is_ssl=False):
        """從連接池獲取連接,或建立新連接"""
        key = (host, is_ssl)
        
        if key not in self.conn_pool:
            if is_ssl:
                self.conn_pool[key] = http.client.HTTPSConnection(host)
            else:
                self.conn_pool[key] = http.client.HTTPConnection(host)
        
        # 檢查連接是否仍然有效
        conn = self.conn_pool[key]
        try:
            # 嘗試使用一個非阻塞的方式檢查連接狀態
            old_timeout = conn.sock.gettimeout()
            conn.sock.settimeout(0.01)
            conn.sock.recv(1, socket.MSG_PEEK)
            conn.sock.settimeout(old_timeout)
        except (socket.error, AttributeError):
            # 連接已關閉或存在問題,創建新連接
            if is_ssl:
                self.conn_pool[key] = http.client.HTTPSConnection(host)
            else:
                self.conn_pool[key] = http.client.HTTPConnection(host)
        except Exception:
            # 其他類型的錯誤,可能連接仍然有效
            pass
            
        return self.conn_pool[key]
    
    def proxy_request(self):
        """使用 http.client 處理代理請求,這在 Python 3.2 中更高效"""
        target_url = urlparse(self.proxy_target + self.path)
        
        # 獲取請求頭部
        headers = {}
        for key, value in self.headers.items():
            # 排除一些特定的頭部
            if key.lower() not in ('host', 'content-length'):
                headers[key] = value
        
        # 設置正確的 Host 頭部
        headers['Host'] = target_url.netloc
        
        # 讀取請求體(如果有)
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else None
        
        try:
            # 確定是否使用 HTTPS
            is_ssl = target_url.scheme == 'https'
            
            # 獲取或創建連接
            conn = self.get_connection(target_url.netloc, is_ssl)
            
            # 構建請求路徑
            request_path = target_url.path
            if target_url.query:
                request_path += '?' + target_url.query
            
            # 發送請求
            conn.request(
                method=self.command,
                url=request_path,
                body=body,
                headers=headers
            )
            
            # 獲取響應
            response = conn.getresponse()
            
            # 設置響應狀態碼
            self.send_response(response.status)
            
            # 設置響應頭部
            for header in response.getheaders():
                key, value = header
                if key.lower() != 'transfer-encoding':  # 排除特定頭部
                    self.send_header(key, value)
            self.end_headers()
            
            # 發送響應體
            self.wfile.write(response.read())
            
            # 不關閉連接,將其保留在連接池中
            
        except http.client.HTTPException as e:
            # 處理 HTTP 錯誤
            self.send_response(500)
            self.send_header('Content-Type', 'text/plain; charset=utf-8')
            self.end_headers()
            self.wfile.write("HTTP Error: {}".format(str(e)).encode('utf-8'))
            
        except Exception as e:
            # 處理其他錯誤
            self.send_response(500)
            self.send_header('Content-Type', 'text/plain; charset=utf-8')
            self.end_headers()
            self.wfile.write("Proxy error: {}".format(str(e)).encode('utf-8'))


def run_server(port=8000, proxy_target="http://localhost:8080", proxy_paths=["/cgi-bin/"], directory=None):
    # 設置代理目標和路徑
    ProxyHTTPRequestHandler.proxy_target = proxy_target
    ProxyHTTPRequestHandler.proxy_paths = proxy_paths
    
    # 確保代理目標是有效的 URL
    if not ProxyHTTPRequestHandler.proxy_target.startswith(('http://', 'https://')):
        ProxyHTTPRequestHandler.proxy_target = "http://{}".format(ProxyHTTPRequestHandler.proxy_target)
    
    # 移除代理目標末尾的斜線(如果有)
    if ProxyHTTPRequestHandler.proxy_target.endswith('/'):
        ProxyHTTPRequestHandler.proxy_target = ProxyHTTPRequestHandler.proxy_target[:-1]
    
    # 設置工作目錄
    if directory:
        os.chdir(directory)
        print("Serving files from directory: {}".format(directory))
    else:
        print("Serving files from current directory: {}".format(os.getcwd()))
    
    # 提高 socket 超時時間以處理慢速連接
    socket.setdefaulttimeout(60)
    
    # 建立服務器(使用線程化服務器)
    server_address = ("", port)
    httpd = ThreadedHTTPServer(server_address, ProxyHTTPRequestHandler)
    
    print("Serving HTTP on 0.0.0.0 port {} (multi-threaded) ...".format(port))
    print("Proxying paths {} to {}".format(proxy_paths, proxy_target))
    
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nServer stopped.")
        httpd.server_close()


if __name__ == "__main__":
    # 解析命令行參數
    parser = argparse.ArgumentParser(description='HTTP Server with proxy capabilities (optimized for Python 3.2)')
    parser.add_argument('--port', type=int, default=8000, help='Port to listen on (default: 8000)')
    parser.add_argument('--proxy-target', type=str, default="http://localhost:8080", 
                        help='Target server to proxy to (default: http://localhost:8080)')
    parser.add_argument('--proxy-paths', type=str, default="/cgi-bin/", 
                        help='Comma-separated list of path prefixes to proxy (default: /cgi-bin/)')
    parser.add_argument('--directory', '-d', type=str, default=None,
                        help='Specify directory to serve files from (default: current directory)')
    
    args = parser.parse_args()
    proxy_paths = [path.strip() for path in args.proxy_paths.split(',')]
    
    run_server(args.port, args.proxy_target, proxy_paths, args.directory)

2017年6月15日 星期四

透過 Nginx proxy_pass 架構,進行 Service migration

有一個舊網站已活了數年,改造的方向不外乎提升 SEO、移除不需要的檔案、增加系統安全、架構拆分,或是基本的 Web Framework 的抽換使用等等。對於一個 IoT 產業來說,包袱多多,像是 device 不見得會更新到新版,更別說會跟隨 HTTP 301/302 去取得新資源,甚至新網站的開發還沒辦法一步到位全部切換,只好透過 Nginx proxy_pass 架構去處理相容並扛流量了。

在此使用 upstream 管理舊機器們,並且添加新的 log format 多紀錄 $upstream_addr 資訊以便後續維護:

log_format add_proxy_pass '$remote_addr - $remote_user [$time_local] [=$upstream_addr=] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"';

resolver 8.8.8.8;
upstream oldserver {
hostname1;
hostname2;
hostname3;
}


後續,都是在 nginx server {} 定義範圍下。

更改 log 記錄格式:

access_log  /var/log/nginx/access.log add_proxy_pass;

預期 http client 能處理 HTTP 301/302 的服務位置,給予更佳的 SEO URL:

location ~ old_page\.php$ {
return 301 $scheme://$host/new/page/;
}


對於不能用 HTTP 301/302 的,則改用 proxy_pass 機制:

location ~ ^/old/resource {
proxy_pass http://oldserver$uri;
}


對於,有些文件很清楚要更改內文關鍵字的,可以善用 ngx_http_sub_module:

location /old/resource/file\.json {
proxy_pass http://oldserver$uri;
sub_filter_types *;
sub_filter_once off;
sub_filter '//old.hostname/' '//cdn.hostname/';
}


也可以順順換成 CDN 架構囉。

2017年6月14日 星期三

AWS 筆記 - AWS ELB 與 Nginx 之 DOS 惡意攻擊者的處理方式

這個緣由是這樣的,有一台 Server 被狂打,但架構是:

Remote client <-> AWS ELB <-> Web server (Nginx) <-> Application

當有用戶非常好心地發大量 request 來檢查機器漏洞,若 Nginx 預設都沒多做設定,那 access.log 都會只記錄到 AWS ELB IP ,也就是不能把 access.log 紀錄的 IP 拿來 ban ,會變成 ban 掉 ELB。

而 AWS 提供的 Security group 預設是從 Allow 的角度,若要特意 ban 掉某個 IP 會有點難搞,例如要改寫防火牆規則,因此,最後就改成從實體 server 去阻擋遠端 client request 了。

步驟一:先讓 Nginx 可以記錄到真實的 remote ip

/etc/nginx/nginx.conf

http {
    # ...
    real_ip_header X-Forwarded-For;
    set_real_ip_from 0.0.0.0/0;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    # ...
}


如此一來,access.log 的 remote_addr 就可以不是 AWS ELB IP 了。

步驟二:用 Nginx 去 deny ip

/etc/nginx/conf.d/your-service.conf

server {
  # ...
  deny RemoteIP;
  # ...
}


這樣設定後,在用 sudo nginx -t 檢查語法後,就可以啟用了

在此不用 iptables 去阻擋的主因是封包進來的 IP 都是 AWS ELB ,所以才改從 Nginx/App 這層取阻擋。缺點就是 access.log 還是一直肥,好處是可以觀察 access.log 看看對方是不是放棄攻擊了?XD

未來若碰到 DDOS 就...

2017年4月19日 星期三

[Vue.js] 使用 webpack / webpack-dev-server 之 Proxy 架構與後端 api 開發整合

使用 Vue.js 開發前端大概是今年的策略,一開始請同事試著把 embedded web interface 重構,碰到的第一個問題是,如何在桌機開發又能向 embedded device 發 request。如此一來,就是可以專心用 vue.js 開發網頁端,而 api 端也可以交給其他人開發,大家有一個共同整合測試的方式。這個在 server site 就是很直觀的 proxy 架構了,很幸運的,webpack 的彈性可以做到。

舉個例來說,我想讓 /json 這個 path 被包裝成回傳某筆資料,在此就用 ipinfo.io 來頂替一下。

https://webpack.js.org/configuration/dev-server/#devserver-proxy

module.exports = {
  entry: {
    app: './src/main.js'
  },
  devServer: {
    proxy: {
       '/json': {
          target: 'https://ipinfo.io/',
          secure: false
       },
    },
  },
...


然而,不知是不是因為我用 vue-cli 的關係:

$ vue init webpack my-project
$ npm list --depth=0
├── autoprefixer@6.7.7
├── babel-core@6.24.1
├── babel-loader@6.4.1
├── babel-plugin-transform-runtime@6.23.0
├── babel-preset-env@1.4.0
├── babel-preset-stage-2@6.24.1
├── babel-register@6.24.1
├── chalk@1.1.3
├── connect-history-api-fallback@1.3.0
├── copy-webpack-plugin@4.0.1
├── css-loader@0.26.4
├── eventsource-polyfill@0.9.6
├── express@4.15.2
├── extract-text-webpack-plugin@2.1.0
├── file-loader@0.10.1
├── friendly-errors-webpack-plugin@1.6.1
├── html-webpack-plugin@2.28.0
├── http-proxy-middleware@0.17.4
├── opn@4.0.2
├── optimize-css-assets-webpack-plugin@1.3.1
├── ora@1.2.0
├── rimraf@2.6.1
├── semver@5.3.0
├── shelljs@0.7.7
├── url-loader@0.5.8
├── vue@2.2.6
├── vue-loader@11.3.4
├── vue-style-loader@2.0.5
├── vue-template-compiler@2.2.6
├── webpack@2.4.1
├── webpack-bundle-analyzer@2.4.0
├── webpack-dev-middleware@1.10.1
├── webpack-hot-middleware@2.18.0
└── webpack-merge@2.6.1


搞了一陣子仍沒成功,且裡頭的確沒 webpack-dev-server :p 最後試了一些招術筆記一下,也看了很多大家再猜來猜去的論點。

總之,筆記兩種解法:

1.原先用 npm run dev 的方式啟動,乾脆改成用 webpack-dev-server 啟動方式,可以確定 devServer.proxy 的 設定有吃到

$ npm install webpack-dev-server --save-dev
$ node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config build/webpack.v.conf.js


2.直接上code! 依照原理實作

$ vim build/dev-server.js
var httpProxy = require("http-proxy");
var apiProxy = httpProxy.createProxyServer();
app.use("/json", function(req, res) {
  //console.log(req);
  req.url = req.baseUrl;
  apiProxy.web(req, res, {
    target: {
      port: 80,
      host: 'ipinfo.io'
    }
  });
});
$ npm run dev


如此一來,都可以解決 :p 可以用 curl -I http://localhost:8080/json 來驗證。

其實改 code 的方式就是不想多測試他的 config 倒底是文件寫錯,還是被改壞,直接靠原理硬解。

2014年6月1日 星期日

[NodeJS] 透過 Wget + HTTP Proxy Server 架構進行 Content 分析

前陣子研究抓取資料時,發現 web crawler 搭配 proxy 時,可以在 proxy 這層進行資料的處理、分析,就一直思考到底要用 python 還是 node.js 測試,最後,實在是 node.js 方便許多,就...衝啦。

在進行之前,小提一下 Proxy Server 本身就有 Internet Content Adaptation Protocol (ICAP) 機制,其實只需要架一個 Squid 在寫一些 ICAP 即可達到類似的效果,有興趣可參考:[Python] 使用 PyICAP 淺玩 ICAP 與 Squid (以 Response Modification / RESPMOD 為例) @ Ubuntu 12.04

對於沒開發過 node.js 的我,就先參考 node-http-proxy 的目錄結構建立起來一個 open source:node-content-filter-proxy

這兩天抽空把玩的心得:
  • 支援 HTTP Requests
  • 尚未處理 HTTPS Requests

抽取 <a href="...">...</a> 用法:


$ cd node-content-filter-proxy/examples && npm install
$ clear;node extract-hmtl-a-href.js
1 Jun 10:20:22 - Service Running at 3128 Port


透過 wget proxy usage:

$ http_proxy="http://localhost:3128" https_proxy="http://localhost:3128" wget -qO -  http://www.google.com/
<a href="http://www.google.com.sg/imghp?hl=en&tab=wi">Images<a/>
<a href="http://maps.google.com.sg/maps?hl=en&tab=wl">Maps<a/>
<a href="https://play.google.com/?hl=en&tab=w8">Play<a/>
...


簡單的說,想要透過既有的 crawler (wget) 去下載資料,但是,資料中我只對 hyperlink 感到興趣,所以透過 proxy server 更新成只回傳 hyperlink 就好,維持 hyperlink 結構可以讓 wget --recursive 抓資料。

如此一來,原先 crawer = wget 架構,轉形成 crawler = wget + proxy server + content analysis 模式,可以專心擺重在內容分析,不必從頭刻一隻類似 wget 的程式。

最後一提,在 extract-hmtl-a-href.js 範例中,採用的不是透過 regular expression 去分析 link ,而是接近一隻 Javascript Rendering Engine (cheerio) ,所以未來可以做的事更多了 :) 至於效率部分倒還好,畢竟 crawler 抓太快會被 ban 掉,此外,分析 content 時,也能設計複雜更高的 seed list (回吐  hyperlink 時),能避開 DOS 現象(例如限定某個 domain 、網站一天只抓2000筆等)。

2014年2月13日 星期四

翻來翻去的筆記: Proxy、VPN 和 SSH Tunnel、iptables port forwarding 混用

???

有想要翻出去,就會有人想要翻進去 XD 在指定國家內架一檯 server ,提供 proxy 、 VPN 服務,提供另一個國家連線,這是一個滿常見的做法,只是,如果國家網路掌控很威,那需要嘗試一些小手段:

前提:

自家機器 IP: localhost , 可用 local_port
指定國家 SERVER IP: x.x.x.x , target service port: xx
第二國家 SERVER IP: y.y.y.y , port forward: yy

SSH Tunnel

假設在指定國家內的 server 服務都是跑 TCP 時,如果直接連它還是不夠順,那就試試 Service over SSH tunnel 吧!

$ ssh -N -L local_port:x.x.x.x:xx x.x.x.x

如此一來,原先的服務就可以改連 localhost:local_port 代替 x.x.x.x:xx 服務。若 ssh tunnel 需要斷掉還能自動重連,在 Ubuntu 就是用 autossh ,把 ssh 指令換成 autossh 即可。若開機就執行,就寫在 /etc/rc.local,另外也用 key-pair 登入來取代密碼輸入:

$ sudo vim /etc/rc.local
su -s /bin/sh account -c 'autossh -N -f -L x.x.x.x:xx x.x.x.x'


有時候單純是 routing table 的問題,需要假藉另一個國家 y.y.y.y 當跳板,這時 TCP/UDP 都可以用 iptables 處理掉(-p tcp 或 -p udp),在 y.y.y.y 機器設定:

$ iptables -A PREROUTING -t nat -i eth0 -p tcp --dport yy -j DNAT --to x.x.x.x:xx
$ iptables -A FORWARD -p tcp -d x.x.x.x --dport xx -j ACCEPT


如此一來,連 y.y.y.y:yy 等同連到 x.x.x.x:xx 。

此外,純 TCP 在 Ubuntu 中還可以透過 redir 套件來處理,若 redir 已安裝好後,好處相較于 iptables 可以不需要 root 權限:

$ redir --lport yy --laddr=y.y.y.y --cport=xx --caddr=x.x.x.x

以 Proxy、VPN 這類服務而言,大概能搞的招數就這些吧,例如 Proxy over SSH tunnel 或甚至 SSH tunnel 是從第二國家建立,以此強制 routing 的改變。