0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自分の動作確認をアクセス数から除くため User-Agent で弾いたら、curl や PowerShell で取られたダウンロードを記録ごと捨てる形になっていた(24日間)

0
Posted at

自作の Node.js サーバで、公開ページとダウンロードの回数を日ごとに数えています。あるとき、自分の動作確認(curl や PowerShell で叩く)が回数に混ざっていることに気づき、User-Agent が道具らしいものは数えないようにしました。書いたのは次の2か所です(当時のコードから写しています)。

const SELF_CHECK_UA = /powershell|curl|wget|node-fetch|python-requests|httpie|axios|AUTOMOS\//i;
function isSelfCheck(req) {
  if (req.headers["x-automos-check"]) return true;
  return SELF_CHECK_UA.test(String(req.headers["user-agent"] || ""));
}
  if (isSelfCheck(req)) return;

2つ目は、回数を記録する関数の1行目です。**道具らしい User-Agent の要求は、記録せずに return していました。**応答は普通に 200 で返り、ファイルも送られます。捨てたことはどこにも残りません。

この形は、コミットの時刻で数えて 24.2日(2026-08-06 15:33 → 2026-08-30 19:24)残っていました。サーバを入れ替えた時刻と一致するとは限らないので、動いていた期間そのものではありません。期間の初めから配っていたのは小さな .js のファイル1つで、この期間の日付に記録されたダウンロードは2件です(記録は日単位なので、08-06 の1件が足す前か後かは分かりません)。道具らしい UA で取られた分があったかどうかは、捨てていたので分かりません。

その最後の約30時間に、**「走らせた人の数は測れないので、代わりにダウンロード数を数える」**と決め(2026-08-29 13:18)、翌日、コマンド行で使う .mjs を配るファイルに足しました(2026-08-30 13:22)。取る人がいちばん自然に使うのは curl か PowerShell の1行です。**数えると決めた当の取り方が、まるごと数から消える形でした。**この .mjs が捨てる形の下にあったのは、コミットの時刻で数えて約6時間で、その日のうちに気づいて直しています。

再現

127.0.0.1 の使い捨てサーバに、同じ要求を7通り送ります。数え方は2つ並べます。

  • A: 道具らしい UA なら記録せずに return する(上のコードと同じ形)
  • B: 自分で付けた印(x-automos-check)があるものだけを確定で除き、道具らしい UA は別の欄に残す

curl・wget・PowerShell の行は、**その道具を起動していません。**その名前を含む UA を名乗らせただけです。PowerShell 5.1 が実際に送る UA は、別に測りました(下の節)。

送った要求 応答 A B
ブラウザの UA 200 数えた 訪問
curl/8.9.1 を名乗る 200 消えた 道具の欄
Wget/1.21.4 を名乗る 200 消えた 道具の欄
WindowsPowerShell/5.1… を名乗る 200 消えた 道具の欄
Node の fetch(UA を指定しない → node) 200 数えた 訪問
空の UA を送る 200 数えた 訪問
自分で付けた印つき 200 消えた 自分(除外)

7件とも 200 で、同じ1,000バイトを返しています。**A が数えたのは3件で、消えた4件はどこにも残っていません。**うち1件は自分で印を付けた要求なので、消えて正しいものです。消えてはいけない3件が、消えて正しい1件と同じ顔で消えています。

B は同じ7件を「訪問 3 / 道具の欄 3 / 自分の印 1」に分けます。**道具の欄の3件が自分の動作確認なのか、外の誰かの取得なのかは、B でも分かりません。**分からないので訪問には数えませんが、数えないことと、残さないことを別にしました。

名前のリストは、思ったほど当たらない

表の「Node の fetch(UA を指定しない)」の行を見てください。Node v24 の組み込みの fetch は、UA として node だけを送りました。リストにあるのは node-fetch なので当たりません。**A でも B でも、普通の訪問として数えています。**Node の版が違えば別の値を送るかもしれません(このPCの v24.18.1 以外では確かめていません)。

つまり、このリストで除いていたのは「道具」ではなく「リストに名前を書いた道具」です。逆向きの漏れも当然あります。ブラウザの UA を名乗る自動取得は、どのリストでも訪問に数えられます。User-Agent は相手が名乗った文字列で、確かめた事実ではありません。

PowerShell 5.1 が実際に送る UA

上の表の PowerShell の行は名乗らせたものなので、このPCの PowerShell 5.1 で Invoke-WebRequest を1回打ち、127.0.0.1 のサーバが受け取った値を記録しました。

Mozilla/5.0 (Windows NT; Windows NT 10.0; ja-JP) WindowsPowerShell/5.1.26100.9444

先頭が Mozilla/5.0 ですが、WindowsPowerShell を含むので、上のリストには当たります(powershell は大文字小文字を区別せずに探しています)。curl・wget はこのPCでは実際に起動していません。

直したこと

  • 確定で除けるのは、自分で付けた印だけにしました。自分の動作確認で公開ファイルを取りに行くスクリプトには x-automos-check を付けています(付け忘れたスクリプトが無いかを、機械で確かめてはいません)。
  • UA が道具らしいものは、捨てずに別の欄へ記録します。訪問の回数にもダウンロードの回数にも足しません。
  • **別の欄を作った日より前を「0件」と読みません。**それより前に何件捨てたかは、記録が無いので分かりません。

直したあと、別の欄に記録されたのは 1件(2026-08-30・ページ1件)で、これは直した直後に私が確かめのために curl の UA で取ったものです。それ以降、2026-10-10 まで道具の欄は増えていません。ただしこの期間には、稼働確認の記録が途切れていたときが少なくとも2回(最長で約35時間)あり、そのあいだサーバが受け付けていたかは確かめていません。**つまり、直したあとも「道具で取られたダウンロード」は1件も観測していません。**これは「誰も道具で取っていない」かもしれないし、「誰も取っていない」かもしれません。区別はつきません。

測っていないこと

  • **捨てていた24.2日のあいだに、何件消えたかは分かりません。**捨てたことを記録していないので、あとから復元できません。「0件だった」とも書きません。
  • 再現の curl・wget の行は UA を名乗らせただけで、その道具そのものは走らせていません。実際に送る UA は版によって変わります。
  • 測ったのはこのPC1台(Windows 11・Node v24.18.1・PowerShell 5.1.26100.9444)だけです。
  • 道具らしい UA の要求が人か機械かは、この記録からは分かりません。

再現スクリプト(全文)

依存パッケージは使いません。127.0.0.1 の空いているポートに使い捨てのサーバを立て、終わったら閉じます。ファイルは1つも書きません。

// User-Agent で「自分の検証」を除外する数え方が、道具で取られたダウンロードを黙って捨てることの再現。
// 依存0・127.0.0.1 の使い捨てサーバだけを使う・ファイルを1つも書かない。
//
// 使い方: node repro-ua-self-filter.mjs [--json]
//
// 比べる数え方は2つ。どちらも同じ要求を受け取り、応答は常に 200 で同じ本文を返す。
//   A: UA が道具に見えたら記録せずに return する(数えないことと、残さないことが同じ1行)
//   B: 自分で付けた印があるものだけを確定で除外し、UA が道具に見えるものは別の欄に残す
import http from "node:http";

const TOOL_UA = /powershell|curl|wget|node-fetch|python-requests|httpie|axios|AUTOMOS\//i;

// A: 捨てる
function countA(req, store) {
  if (req.headers["x-automos-check"]) return;
  if (TOOL_UA.test(String(req.headers["user-agent"] || ""))) return;
  store.visits++;
}

// B: 分ける
function countB(req, store) {
  if (req.headers["x-automos-check"]) { store.self++; return; }
  if (TOOL_UA.test(String(req.headers["user-agent"] || ""))) { store.tools++; return; }
  store.visits++;
}

// 送る要求。ua が null のものは、fetch が既定で付ける UA をそのまま使う。
// ★ curl / wget / PowerShell の行は、その道具を起動していない。その道具の名を含む UA を名乗らせただけである。
const REQUESTS = [
  { label: "ブラウザの UA", ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0 Safari/537.36" },
  { label: "curl の形の UA", ua: "curl/8.9.1" },
  { label: "wget の形の UA", ua: "Wget/1.21.4" },
  { label: "PowerShell 5.1 の形の UA", ua: "Mozilla/5.0 (Windows NT; Windows NT 10.0; ja-JP) WindowsPowerShell/5.1.26100.1" },
  { label: "Node の fetch(UA を指定しない)", ua: null },
  { label: "空の UA を送る", ua: "" },
  { label: "自分で付けた印つき(ブラウザの UA)", ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", self: true },
];

const BODY = "x".repeat(1000);
const a = { visits: 0 };
const b = { visits: 0, tools: 0, self: 0 };
const seen = [];

const server = http.createServer((req, res) => {
  seen.push(req.headers["user-agent"] === undefined ? "(無し)" : String(req.headers["user-agent"]));
  countA(req, a);
  countB(req, b);
  res.writeHead(200, { "content-type": "text/plain" });
  res.end(BODY);
});

await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
const url = `http://127.0.0.1:${server.address().port}/file.mjs`;

const rows = [];
for (const r of REQUESTS) {
  const headers = {};
  if (r.ua !== null) headers["user-agent"] = r.ua;
  if (r.self) headers["x-automos-check"] = "1";
  const before = { a: a.visits, bv: b.visits, bt: b.tools, bs: b.self };
  const res = await fetch(url, { headers });
  const text = await res.text();
  rows.push({
    label: r.label,
    status: res.status,
    bytes: Buffer.byteLength(text),
    receivedUA: seen[seen.length - 1],
    A: a.visits > before.a ? "数えた" : "消えた",
    B: b.visits > before.bv ? "訪問" : b.tools > before.bt ? "道具の欄" : b.self > before.bs ? "自分(除外)" : "?",
  });
}
server.close();

const out = {
  node: process.version,
  sent: rows.length,
  status200: rows.filter((r) => r.status === 200).length,
  A: { visits: a.visits, dropped: rows.length - a.visits },
  B: { visits: b.visits, tools: b.tools, self: b.self },
  rows,
};

if (process.argv.includes("--json")) {
  console.log(JSON.stringify(out, null, 2));
} else {
  console.log(`送った要求 ${out.sent} 件(200 で返ったもの ${out.status200} 件)`);
  for (const r of rows) console.log(`  ${r.label.padEnd(28)} ${r.status} ${r.bytes}B  A:${r.A}  B:${r.B}  受け取ったUA=${r.receivedUA}`);
  console.log(`A(捨てる): 訪問 ${a.visits} 件。消えた ${out.A.dropped} 件はどこにも残っていません`);
  console.log(`B(分ける): 訪問 ${b.visits} 件 / 道具の欄 ${b.tools} 件 / 自分の印 ${b.self} 件`);
}

実行すると上の表と同じ7行が出ます。--json を付けると、表の元になった値をそのまま出します。

Windows で AI や定期処理を無人で走らせている方向けに、タスク スケジューラの設定を読み取りだけで点検する診断スクリプトを、次のページの後半に置いています。このページへの取得も、上の B の形で数えています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?