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

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)

2024年7月18日 星期四

PHP 開發筆記 - 研究 osTicket plugins 架構,以一個可擴充前端資源 plugin 為例


之前團隊在擴充 osTicket 時,主要是下海改他的 code ,但 osTicket 本身也有 plugin 架構,就花點時間看一下資料,感覺比想像中少資料可以看,只好直接看官方的 code 猜有什麼架構可用。

資料:
差不多了,然後看一下這幾個實作,盡量找最簡單的
剛好團隊內以前也都有開發過 oauth, ldap 整合,所以快速掃一下就可以很快摸索出個大概,重要的資訊: 
  • plugin 主要由 3 個檔案組成,分別是 plugin.php, config.php, yourclass.php
  • plugin 要儲存一些設定值,只需 config.php 內提供 function getAllOptions() {} 就可以輕鬆達成
  • plugin 可以自己再創個 db table 來儲存資料,一樣嘢是在 config.php 中伺機創結構
  • plugin 安插自己的程式碼主要是依賴 Signals API 架構很漂亮,但現有 Signals API 的入口點並沒有太多 UI 擴充點
  • plugin 可以製作額外的 API ,請參考 audit plugin 內的實作片段

就這樣,可以摸索個大概:

```
osTicket % grep -r "Signal::" * | grep -o 'Signal::[^,]*' | sort | uniq
Signal::connect('api'
Signal::connect('auth.clean'
Signal::connect('cron'
Signal::connect('model.created'
Signal::connect('model.deleted'
Signal::connect('model.updated'
Signal::connect('object.deleted'
Signal::connect('organization.created'
Signal::connect('session.close'
Signal::connect('signal.name'
Signal::connect('staff.header.extra'
Signal::connect('system.install'
Signal::connect('threadentry.created'
Signal::connect('ticket.created'
Signal::connect('user.auth'
Signal::connect('user.created'
Signal::connect() function call
Signal::connect\('([^']+)'/m"
Signal::send($action
Signal::send('agent.audit'
Signal::send('agenttab.audit'
Signal::send('ajax.client'
Signal::send('ajax.scp'
Signal::send('api'
Signal::send('apps.admin'
Signal::send('apps.scp'
Signal::send('auth.clean'
Signal::send('auth.login.failed'
Signal::send('auth.login.succeeded'
Signal::send('auth.logout'
Signal::send('auth.pwchange'
Signal::send('auth.pwreset.email'
Signal::send('auth.pwreset.login'
Signal::send('config.ttfonts'
Signal::send('cron'
Signal::send('export.tables'
Signal::send('mail.decoded'
Signal::send('mail.received'
Signal::send('model.created'
Signal::send('model.deleted'
Signal::send('model.updated'
Signal::send('object.created'
Signal::send('object.deleted'
Signal::send('object.edited'
Signal::send('object.view'
Signal::send('organization.created'
Signal::send('person.login'
Signal::send('person.logout'
Signal::send('session.close'
Signal::send('signal.name'
Signal::send('syslog'
Signal::send('system.install'
Signal::send('task.created'
Signal::send('threadentry.created'
Signal::send('ticket.create.before'
Signal::send('ticket.create.validated'
Signal::send('ticket.created'
Signal::send('ticket.view.more'
Signal::send('user.audit'
Signal::send('user.created'
Signal::send('user.login'
Signal::send('usertab.audit'
Signal::send() for the same named signal.
Signal::send() for the same-named signal.
Signal::send\('([^']+)'/m"
```

以及弄出個簡單的 osTicket plugin 來記錄一下:

搭配 osTicket 程式碼變動: 

% cat -n osTicket/include/staff/header.inc.php | head -n 64 | tail -n 10

    55     <link rel="icon" type="image/png" href="<?php echo ROOT_PATH ?>images/oscar-favicon-16x16.png" sizes="16x16" />

    56

    57     <?php

    58     Signal::send('staff.header.extra');

    59     if($ost && ($headers=$ost->getExtraHeaders())) {

    60         echo "\n\t".implode("\n\t", $headers)."\n";

    61     }

    62     ?>

    63 </head>


此 plugin 目的是提供引入更多 js, css resources,但如同上述提到的 osTicket plugin 沒有提供太多前端插入的 Signals ,自己得多添加一個 `Signal::send('staff.header.extra', null);` ,再來提 feature requests 來詢問開發團隊,看看是不是自己誤會了