中国では色々なサイトが政府規制により閲覧できない。
また日本広告主協会Web広告研究会が2005年12月13日に発表した調査結果によると、社内PCから閲覧できるWebサイトを制限している企業が7割にのぼっている。
規制方法には「URLフィルタリング」や「キーワードによる規制」など色々とある。
見ようと思っているサイトが閲覧できないというのは非常にストレスだ。
だからといってスマホを取り出して閲覧するのは明らかに遊んでそうだしパソコンでみたいよね。
「URLフィルタリング」の場合の閲覧規制回避策は「大人のCGIスクリプト (ハッカージャパンBOOKS 3)」で学生時代に学習済。
具体的には規制の範囲外にあるHTTPのProxyサーバ経由でアクセスすることで、URLフィルタリングを回避するというもの。
これを使って閲覧規制のあるサイトは閲覧していたけど、CSSや画像が読み込めず20年ぐらい諦めていた。
今回、LLM様のおかげで画像やCGIも読み込み可能となり更にChrome extensionからの呼び出し対応を実現できたので、クリック一発で閲覧可能となった。
サーバーを使用するプロキシ方式をPHPで実現する
HTTPプロトコルは、リクエストとレスポンスのデータをストリーム(連続したデータの流れ)として扱うことができる。
これにより、サーバーは以下のような動作が可能になる。
- ターゲットサーバーから受け取ったデータを、保存せずにそのままクライアントに転送
- データの受信と送信を並行して行う(リアルタイムで中継する)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
<?php if (phpversion() >= "4.1.0") { // register_globals が off のため extract($_POST); extract($_GET); extract($_SERVER); } // PHP 5.4 からはgzdecode関数が使える function gzDecodeWithTmpFile($data) { $gzfile = tempnam('/tmp', 'gzdecode'); file_put_contents($gzfile, $data); ob_start(); readgzfile($gzfile); $result = ob_get_clean(); unlink($gzfile); // ファイルを削除 return $result; } // CSS内の相対パスを絶対パスに変換 function convertRelativePaths($css_content, $base_url) { return preg_replace_callback( '/url\(["\']?(.*?)["\']?\)/i', function ($matches) use ($base_url) { $url = $matches[1]; if (strpos($url, 'http') === 0) { return $matches[0]; } elseif (strpos($url, '/') === 0) { return 'url(' . dirname($base_url) . $url . ')'; } else { return 'url(' . dirname($base_url) . '/' . $url . ')'; } }, $css_content ); } // 443でアクセス function httpget($target_url) { $url_array = parse_url($target_url); if ($url_array['scheme'] == "https") { $host = $url_array['host']; $hostb = 'ssl://' . $host; $port = 443; $addr = $url_array['path']; } else { $host = $url_array['host']; $hostb = $host; $port = 80; $addr = $url_array['path']; } $request = "GET {$addr} HTTP/1.1\r\n" . "Accept: */*\r\n" . "Host: {$host}\r\n" . "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100\r\n" . "Content-Type: application/x-www-form-urlencoded\r\n" . "Connection: close\r\n\r\n"; if ($fp = fsockopen($hostb, $port)) { fwrite($fp, $request); // ヘッダーを取得 $header = ''; while (!feof($fp) && strpos($header, "\r\n\r\n") === false) { $header .= fgets($fp, 512); } // レスポンスのボディを取得 $response = ''; while (!feof($fp)) { $response .= fgets($fp); } fclose($fp); // CSSファイルの場合、相対パスを絶対パスに変換 if (strpos($header, 'Content-Type: text/css') !== false) { header('Content-Type: text/css'); $response = convertRelativePaths($response, $target_url); } // HTML内のリンクをプロキシ経由に変換 $response = preg_replace_callback( '/(src|href)=["\'](.*?)["\']/i', function ($matches) use ($url_array) { $url = $matches[2]; if (strpos($url, 'http') === 0) { return $matches[1] . '="index.php?u=' . urlencode($url) . '"'; } elseif (strpos($url, '/') === 0) { return $matches[1] . '="index.php?u=' . urlencode($url_array['scheme'] . '://' . $url_array['host'] . $url) . '"'; } else { return $matches[1] . '="index.php?u=' . urlencode($url_array['scheme'] . '://' . $url_array['host'] . '/' . $url) . '"'; } }, $response ); return $response; } return false; } if (isset($u)) { echo $data = httpget($u); exit; } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>PHP Proxy</title> <meta name="ROBOTS" content="NOINDEX,NOFOLLOW"> <meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noarchive"> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> <style> body { background-color: #f8f9fa; color: #343a40; } .container { margin-top: 50px; } .form-control { margin-bottom: 15px; } .btn-primary { background-color: #007bff; border-color: #007bff; } .btn-primary:hover { background-color: #0056b3; border-color: #004085; } </style> </head> <body> <div class="container"> <div class="card"> <div class="card-header"> <h2>PHP Proxy</h2> </div> <div class="card-body"> <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET"> <div class="form-group"> <label for="url">URL: <input type="text" class="form-control" id="url" name="u" value="" required> </div> <div class="form-group"> <label for="ref">Ref: <input type="text" class="form-control" id="ref" name="ref" value="" required> </div> <button type="submit" class="btn btn-primary">Start</button> </form> </div> </div> </div> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> </body> </html> |
実際に作成されたWebページはこんな感じ。
ここでURLとリファラーを入れたらアクセス可能になる。
技術的なお話
プロキシを通じてHTMLを読み込む場合、リンク先のリソース(画像や他のページ)もプロキシ経由でアクセスする必要がある。
そこで次のような対応が必要だ。
- CSSファイルの相対パスを絶対パスに変換
–convertRelativePaths
関数を使用して、CSS内の相対パスを絶対パスに変換。 - HTML内のリンクをプロキシ経由に変換
–preg_replace_callback
を使用して、src
やhref
属性をプロキシ経由のURLに変換。 - CSSファイルのMIMEタイプを設定
–Content-Type: text/css
を明示的に設定し、ブラウザがCSSとして認識できるようにする。
chrome 拡張機能を使ってクリック一発で閲覧できるようにする
長い間「Chrome拡張」を自作したいと思っていたが、そもそも用途やアイデアが浮かばなかったし他人の作った拡張機能が優秀過ぎて「Hello world」で終わっていた。
今回のような自作HTTPサーバを呼び出すChrome拡張なんて存在しないだろうし基本が身につくので実装してみた。
ファイル構成はこんな感じ。
Hello_Extensions/
├── manifest.json
├── background.js
├── popup.html
└── icon.png
popup.html
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!DOCTYPE html> <html> <head> <title>URL Reopen Extension</title> </head> <body> <h1>URL Reopen Extension</h1> 現在のタブのURLをプロキシ経由で開き直します。 </body> </html> |
background.js
1 2 3 4 |
chrome.action.onClicked.addListener((tab) => { const proxyUrl = "https://●●●●●●●●/proxy?u=" + encodeURIComponent(tab.url); chrome.tabs.update(tab.id, { url: proxyUrl }); }); |
manifest.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{ "manifest_version": 3, "name": "URL Reopen Extension", "version": "1.0", "description": "特定のURLをPHPプロキシ経由で開き直す拡張機能", "permissions": ["activeTab", "scripting"], "host_permissions": [" "background": { "service_worker": "background.js" }, "action": { "default_icon": { "16": "icon.png", "48": "icon.png", "128": "icon.png" } } } |
icon.png
128×128画素のPNG画像。Hello world作った時のサンプルの再利用。
Chrome拡張インストール手順
- 1. Chromeの「拡張機能」ページを開く
- 2. 「デベロッパーモード」をオンにする
- 3. 「パッケージ化されていない拡張機能を読み込む」をクリックし、
Hello_Extensions
フォルダを選択する
これでChrome拡張が表示されるようになる。
そして閲覧できなかったサイトでボタンを押すと閲覧可能。
技術的な説明
拡張機能を実装する際、最初に理解しなければならないのが、拡張機能が持つ3種類のプログラムとその(実行)コンテキスト、および互いのやり取り(メッセージング)の方法。
- background scripts
拡張機能固有の実行コンテキストで動作するプログラムで、特定のタブに依存せず、各タブを横断した制御を行うことができる。 - content scripts
Webサイトの実行コンテキストで動作するプログラム。URLやタイミングは、manifest.jsonやbackground scriptsで制御 - popup html
拡張機能のアイコンを押した際にポップアップ表示されるWebコンテンツで、独立したコンテキストを持っている
今回は popup.html を使用されていないため、削除しても動作に影響はない。
おわりに
LLMを使うと簡単に作れちゃう。
そもそもコード自体は100行前後の簡単なものだけど、これを調査しながら作るとすると大変。
人間に必要なのはアイデア。
アイデアが続く限り何かを作り続けたいと思っているけれど、最近はやりたい事は実現できるようになりネタが出なくなった。