curl コマンド使って、Webサイトの挙動を確認する

年末に流れた、以下の tweet。

確かに、Google の検索結果に表示されるリンクをクリックすると、新しい URL へ転送されるので、旧サイト→新サイトへの転送は正しく設定されていることはわかるのだけど、どうやら Google などのクローラが望む形での転送設定ではないようなので、どうなっているのかを調べてみた。
サイト自体は10月までに新しい URL に移行しているそうで、

未だに旧サイトしか検索結果に上がってこないのは如何なものか、という事もあったりするんですけど。

こういう時、Web ブラウザだけでは挙動を追いきれないので、自分は curl コマンドを使って確認することがほとんど。
curl コマンドは、URL 形式で表現( http://〜 や ftp://〜 のような表記のやつ)されるサーバのロケーションへのアクセスしてデータを取得するためのものなのだけど、単純にデータを取ってくるだけ以外の用途でも使えるので、今回は Web サイトの挙動をチェックする際の使い方に絞って、簡単に解説してみる。

まずは、手始めに Web アクセスした時のクライアントからのリクエストの内容と、それに対して返ってくるサーバからのレスポンスの内容をチェックする方法。
これは、 -v オプションを付けて curl を実行すれば OK。

soukaku@vhost01:[~]$ curl -v http://law.e-gov.go.jp/htmldata/H11/H11HO042.html
* Trying 2400:4040:5013:b::13...
* TCP_NODELAY set
* Trying 210.232.23.44...
* TCP_NODELAY set
* Connected to law.e-gov.go.jp (210.232.23.44) port 80 (#0)
> GET /htmldata/H11/H11HO042.html HTTP/1.1
> Host: law.e-gov.go.jp
> User-Agent: curl/7.57.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: close
< Cache-Control: no-cache
< Content-Type: text/html; charset=shift_jis
< Pragma: no-cache
< Content-Length: 189
<
* Closing connection 0
<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 10052633866947281389</body></html>

行頭に ">" があるのがリクエストヘッダの内容で、"<" があるのがレスポンスヘッダ。この場合の最後の1行が、実際に Web サーバ側から送られてきたコンテンツの中身(= HTML 形式のデータ)というわけなんだけど、そこを見ると "Request Rejected" とあるので、どうも curl でアクセスすると、上手くアクセスがが出来ないようにサーバ側が設定されているっぽい。


だったら、ということでユーザエージェント名を任意のものに(要するに偽装)するために -A オプションで適当なブラウザのエージェント名を指定してアクセスしてみる。もちろん適当と言っても、 Web ブラウザ個別で持っているエージェント名を使う必要があるんですが。
今回は Firefox の macOS 版のユーザエージェント情報を指定してアクセスしてみたところ、

soukaku@vhost01:[~]$ curl -v http://law.e-gov.go.jp/htmldata/H11/H11HO042.html -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0"
* Trying 2001:c80:a100:b::13...
* TCP_NODELAY set
* Trying 210.232.23.44...
* TCP_NODELAY set
* Connected to law.e-gov.go.jp (210.232.23.44) port 80 (#0)
> GET /htmldata/H11/H11HO042.html HTTP/1.1
> Host: law.e-gov.go.jp
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-length: 278
< Content-type: text/html
< Date: Sun, 31 Dec 2017 11:23:33 GMT
< Last-modified: Mon, 02 Oct 2017 07:27:25 GMT
< Accept-ranges: bytes
< X-frame-options: SAMEORIGIN
< X-content-type-options: nosniff
< Via: 1.1 proxy-law
< Proxy-agent: none
< Set-Cookie: TS01d766af=01a3c6b6d54526083f0600c3e4419f3823d7118b340f8a778c3cfa954da9931c1fcb3084a7; Path=/
<
* Connection #0 to host law.e-gov.go.jp left intact
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>e-GovSearch</title></head><script>function Jump(){location.href = "http://elaws.e-gov.go.jp/search/elawsSearch/elaws_search/lsg0500/detail?lawId=411AC0000000042&openerCode=1";}</script><body onLoad="Jump();"></body></html>

という結果になりました。
リクエストヘッダ中の "User-Agent:" が指定した通りのエージェント名になったことで、サーバ側も "Firefox からのリクエスト" と認識したので、"Se-Cookie:" がレスポンス中に含まれたものになり、送られてきた HTML の内容も先のものとは変わってます。

送られてきた HTML の内容からすると、旧サイト側にアクセスが有った場合は、それに該当する新サイトの URL へ転送をするための JavaScript を送ってくるので、送られてきた内容を解釈してさらにアクセスが出来るもの(= Web ブラウザ)であれば、転送先を見ることが出来るというわけですね。

curl もそうですが、 Google などのクローラだとコンテンツの中身を見て転送先まで追いかけるということはしてくれないので、 SEO という観点で見た場合はちょっと問題になるわけです。
それを避けるために、サイト移転で URL が変わったときは、 コンテンツ内に転送する仕組みを入れるのではなく、 httpd 側で旧 URL へのアクセスには HTTP ステータスコード "301 Moved Permanently" を返しつつ、レスポンスヘッダ中に "Location: 新 URL" を返すという設定を行えば、大抵の検索エンジンはよしなに対応してくれるんですけどね。Web ブラウザも ステータスコード 301 と "Location:" ヘッダは正しく解釈してくれるので、人力でのアクセスにも対処できるんですが…。

更にいうと、今回の場合は、

とで、robots.txt を使ってクローラによるアクセスを拒否する形になっているため、よしんば httpd による転送設定が行われていたとしても、クローラは全部弾かれてしまうというわけで…。
#どうしてこうなった感が…。

では、ステータスコード 301 、 Location ヘッダで転送先が送られてきた場合の挙動はどうなるかというと、 curl はそれに対応しているので問題なくアクセスすることが出来ます。
例えば、短縮 URL サービスで短縮された URL へのアクセスを行った場合。

tweet 中の URL がリンクになっていて、そこを右クリックの「リンクをコピー」を使って、"https://t.co/IzE6TJB6cW" という URL を得て、それを curl に渡してアクセス。今回は、転送先まで追いかけたいので -L オプションを追加。

soukaku@vhost01:[~]$ curl -vL https://t.co/IzE6TJB6cW
* Trying 104.244.42.5...
* TCP_NODELAY set
* Connected to t.co (104.244.42.5) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: businessCategory=Private Organization; jurisdictionC=US; jurisdictionST=Delaware; serialNumber=4337446; C=US; ST=California; L=San Francisco; O=Twitter, Inc.; OU=tsa_m Point of Presence; CN=t.co
* start date: Jul 25 00:00:00 2017 GMT
* expire date: Jul 30 12:00:00 2018 GMT
* subjectAltName: host "t.co" matched cert's "t.co"
* issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=DigiCert SHA2 Extended Validation Server CA
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x55d24dee1950)
> GET /IzE6TJB6cW HTTP/2
> Host: t.co
> User-Agent: curl/7.57.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 301
< cache-control: private,max-age=300
< content-length: 0
< date: Sun, 31 Dec 2017 09:33:03 GMT
< expires: Sun, 31 Dec 2017 09:38:03 GMT
< location: http://gcm.qtmoni.bosai.go.jp/test2017/lp/
< server: tsa_m
< set-cookie: muc=fb898bed-94e8-41a3-96d7-26d4910ef132; Expires=Tue, 31 Dec 2019 09:33:03 UTC; Domain=t.co
< strict-transport-security: max-age=0
< vary: Origin
< x-connection-hash: 5f1ae063e4bb25691dbe3f271ebe3684
< x-response-time: 115
<
* Connection #0 to host t.co left intact
* Issue another request to this URL: 'http://gcm.qtmoni.bosai.go.jp/test2017/lp/'
* Trying 158.203.239.205...
* TCP_NODELAY set
* Connected to gcm.qtmoni.bosai.go.jp (158.203.239.205) port 80 (#1)
> GET /test2017/lp/ HTTP/1.1
> Host: gcm.qtmoni.bosai.go.jp
> User-Agent: curl/7.57.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 31 Dec 2017 09:33:03 GMT
< Server: Apache/2.2.15 (CentOS)
< Last-Modified: Wed, 15 Nov 2017 02:21:38 GMT
< ETag: "e0487-5b6-55dfc29cd1080"
< Accept-Ranges: bytes
< Content-Length: 1462
< X-Cnection: close
< Content-Type: text/html; charset=UTF-8
<
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>地震モニタアプリ ベータテスター募集</title>

<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">

<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<meta http-equiv="refresh" content="0;URL=http://www.rcsc.co.jp/quake-monitor-lp">
</head>
<body>
<img src="logo.gif">
<hr>
<div>
ベータテスター募集ページに自動的にジャンプしない場合は<a href="http://www.rcsc.co.jp/quake-monitor-lp">こちら</a>
</div>

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
</body>
* Connection #1 to host gcm.qtmoni.bosai.go.jp left intact
</html>

今回の場合は、 t.co にアクスすることでステータスコード 301 と Location ヘッダにより転送先の URL が返って来て、 curl が自動的に Location ヘッダにある URL にアクセスして HTML まで取得してきているのが判ると思います。転送先から返ってきたコンテンツの中身を見ると HTML の meta タグ使った転送処理をかけてるんですね…。
#go.jp のサイトに転送して、さらにそこから企業サイトに転送するのって、どうなの?とは思ってしまうが…。

あと、t.co との SSL/TLS セッションの部分も出力されていて( GET リクエストを投げる前の部分)、そこをみれば 相手側のサーバ証明書の内容や使われる暗号スイートとその強度なんかがわかります。

また、 t.co は HTTP2 に対応しているため、GET リクエストを投げる前の段階の SSL/TLS コネクションのところで "Using HTTP2, server supports multi-use" と HTTP2 でのアクセスが出来るということを宣言しています。なので curl もそれに合わせて GET リクエスト時に "GET /IzE6TJB6cW HTTP/2" というかたちで HTTP2 でのアクセスであることを明示して処理してます。
但し、curl 自体が HTTP2 に対応するようにコンパイルされていない場合はこのような動作はしません。 HTTP2 に対応しているかどうかは -V を付けて実行するとわかります。以下は、macOS High Sierra の場合。

[soukaku@messiah|~]$ /usr/bin/curl -V
curl 7.54.0 (x86_64-apple-darwin17.0) libcurl/7.54.0 LibreSSL/2.0.20 zlib/1.2.11 nghttp2/1.24.0
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz HTTP2 UnixSockets HTTPS-proxy

curl 自体、 RHEL/CentOS 、 Debian 、 macOS ならデフォルトで入ってたり、入っていなかったとしても比較的簡単にインストールできますんで、入っていたらあとは man curlcurl --help でどんな事ができるのか確認しておくと良いと思います。 Windows 10 なら Windows Subsystem for Linux の次期アップデートに curl が含まれるそうですし…。

さて以下は、おまけのようなもんですが、HTTPS なサイトにアクセスすると、時々 SSL 証明書が不正なサイトだった場合に Web ブラウザが警告を出す場合がありますが、 curl もそういうサイトの場合は、

soukaku@vhost01:[~]$ curl -vIL https://blog.csf.or.jp/
* Trying 52.196.4.0...
* TCP_NODELAY set
* Connected to blog.csf.or.jp (52.196.4.0) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, Server hello (2):
* SSL certificate problem: self signed certificate
* stopped the pause stream!
* Closing connection 0
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

こんな感じで、SSL/TLS セッションの段階で接続が切れます。返ってきたメッセージ見ると "curl: (60) SSL certificate problem: self signed certificate" とあって、どうやら自己証明書(いわゆるオレオレ証明書)を使っているので切断されたようです。

こういったエラーを回避して HTTPS サイトにアクセスするには、 -k オプションを付けて実行すればよいので、

soukaku@vhost01:[~]$ curl -vIL https://blog.csf.or.jp/ -k
* Trying 52.196.4.0...
* TCP_NODELAY set
* Connected to blog.csf.or.jp (52.196.4.0) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=www.example.com
* start date: Sep 5 18:54:02 2016 GMT
* expire date: Sep 3 18:54:02 2026 GMT
* issuer: CN=www.example.com
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> HEAD / HTTP/1.1
> Host: blog.csf.or.jp
> User-Agent: curl/7.57.0
> Accept: */*
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Sun, 31 Dec 2017 14:03:21 GMT
Date: Sun, 31 Dec 2017 14:03:21 GMT
< Server: Apache
Server: Apache
< X-Powered-By: PHP/7.0.10
X-Powered-By: PHP/7.0.10
< Link: <https://blog.csf.or.jp/wp-json/>; rel="https://api.w.org/", <https://wp.me/82FMS>; rel=shortlink
Link: <https://blog.csf.or.jp/wp-json/>; rel="https://api.w.org/", <https://wp.me/82FMS>; rel=shortlink
< X-Frame-Options: SAMEORIGIN
X-Frame-Options: SAMEORIGIN
< Cache-Control: max-age=0, no-cache
Cache-Control: max-age=0, no-cache
< Content-Type: text/html; charset=UTF-8
Content-Type: text/html; charset=UTF-8

<
* Connection #0 to host blog.csf.or.jp left intact

と、相手からのレスポンスまで得ることが出来ました。
このサイトの場合、 -k オプション無しでエラーが出る原因は、 URL のドメイン名とサーバ証明書の Common Name が異なっている ためのようですね。
#いつになったら、まともなサーバ証明書を用意するんだろう…。

上のように、相手からのレスポンスヘッダまで見れれば良くてコンテンツの内容が不要な場合は、 -I オプションを付けて実行すれば HEAD メソッドでのアクセスとなります。

まぁ、ここまでの説明で出てきたオプションを覚えておけば、大抵の場面で困らないんじゃないかなぁ、と思います。個人的にも "curl -vIL 任意のURL" を一発目に試して、状況によって -k オプションを追加するという感じの使い方が殆どですね。実際のところ、プロキシ通さないと Web アクセスできない環境でのテストもあったりするので --proxy オプションと --proxy-user オプションも多用していますが。

トラックバック(0)

コメントする