LoginSignup
2
0

More than 5 years have passed since last update.

PHPとGo言語のURLパーサについて

Last updated at Posted at 2018-12-12

このエントリーはGMOアドマーケティング Advent Calendar 2018の13日目です。前日は@zakisanbaimanさんのOWASP ZAPでWebアプリケーション脆弱性診断でした。

Advent Calendar を書くにあたり、何をかこうか検討しましたが、普段使っている上で気になったPHPの組み込み関数である parse_url について書きたいと思います。
また、その比較対象はGo言語を選択しました。選択理由は私がGo言語をいま勉強しているからです。

なぜ、 parse_url について記事を書くのか

これは単純で動作が独特だと感じたためです。
※個人の感想です。

フルフルてんこ盛りURL

parse_urlのマニュアル に例として載っている下記URLをパースしてみます。

'http://username:password@hostname:9090/path?arg=value#anchor'

こちらをGo言語とPHPでパースするとどうなるのか。

var_dump(parse_url('http://username:password@hostname:9090/path?arg=value#anchor'));
//[
//  'scheme' => string(4) "http"
//  'host' => string(8) "hostname"
//  'port' => int(9090)
//  'user' => string(8) "username"
//  'pass' => string(8) "password"
//  'path' => string(5) "/path"
//  'query' => string(9) "arg=value"
//  'fragment' => string(6) "anchor"
//]

ではGo言語ではどのように書くのかといいますと

package main

import (
    "fmt"
    "net/url"
)

func main() {
    u, _ := url.Parse("http://username:password@hostname:9090/path?arg=value#anchor")
    pass, _ := u.User.Password()
    fmt.Printf("scheme: %s\n", u.Scheme)
    fmt.Printf("host: %s\n", u.Hostname())
    fmt.Printf("port: %s\n", u.Port())
    fmt.Printf("user: %s\n", u.User.Username())
    fmt.Printf("pass: %s\n", pass)
    fmt.Printf("path: %s\n", u.Path)
    fmt.Printf("query: %s\n", u.RawQuery)
    fmt.Printf("fragment: %s\n", u.Fragment)
}
//scheme: http
//host: hostname
//port: 9090
//user: username
//pass: password
//path: /path
//query: arg=value
//fragment: anchor

結果は期待した通り同じになりました。

プロトコルを省略した場合もやってみます。

var_dump(parse_url('//hostname/path?arg=value'));
//[
//  'host' => string(8) "hostname"
//  'path' => string(5) "/path"
//  'query' => string(9) "arg=value"
//]

PHPではプロトコルを省略した場合もURLとして処理されている事が確認できます。
Go言語も同様に省略した部分は考慮している事が確認できます。

u, _ := url.Parse("//hostname/path?arg=value")
pass, _ := u.User.Password()
fmt.Printf("scheme: %s\n", u.Scheme)
fmt.Printf("host: %s\n", u.Hostname())
fmt.Printf("port: %s\n", u.Port())
fmt.Printf("user: %s\n", u.User.Username())
fmt.Printf("pass: %s\n", pass)
fmt.Printf("path: %s\n", u.Path)
fmt.Printf("query: %s\n", u.RawQuery)
fmt.Printf("fragment: %s\n", u.Fragment)
//scheme: 
//host: hostname
//port: 
//user: 
//pass: 
//path: /path
//query: arg=value
//fragment: 

こちらも予想した通りになりました。
次にプロトコルを想定外なものに設定してみます。

var_dump(parse_url('s://hostname/path'));
//[
//  'scheme' => string(1) "s"
//  'host' => string(8) "hostname"
//  'path' => string(5) "/path"
//]

続いてGo言語です。

u, _ := url.Parse("s://hostname/path")
fmt.Printf("scheme: %s\n", u.Scheme)
fmt.Printf("host: %s\n", u.Hostname())
fmt.Printf("path: %s\n", u.Path)
//scheme: s
//host: hostname
//path: /path

s: はテストコードを書くために雑に先頭の http を削った所、動いてしまったのでGo言語でも試してみましたが、同じ結果になりました。
続けます。

var_dump(parse_url('://hostname/path'));
//[
//  'path' => string(16) "://hostname/path"
//]

host が消失しました。期待した結果になりませんでしたね。

_, err := url.Parse("://hostname/path")
if err != nil {
    fmt.Print(err)
    return
}
//parse ://hostname/path: missing protocol scheme

Go言語ではパースに失敗しました。個人的にはこちらの方が期待した動きです。
RFC3986を確認してみましょう。

Scheme names consist of a sequence of characters beginning with a
letter and followed by any combination of letters, digits, plus
("+"), period ("."), or hyphen ("-").  Although schemes are case-
insensitive, the canonical form is lowercase and documents that
specify schemes must do so with lowercase letters.  An implementation
should accept uppercase letters as equivalent to lowercase in scheme
names (e.g., allow "HTTP" as well as "http") for the sake of
robustness but should only produce lowercase scheme names for
consistency.

引用元| https://tools.ietf.org/html/rfc3986#section-3.1

RFC3986を見る限りではありますが、スキーマの定義を満たしていない事は明らかですね。
そのため、Go言語ではエラーにしているのでしょう。ただ、そうなると先に紹介した //hostname/path?arg=value はパースできた事が気になりますね。
ただ、今回はPHPについて書きたいと思ったので、Go言語についての追求はここまでとしましょう。
PHPではおそらくスキーマの定義を満たしていないため、 schema が空となったと思わます。

最初に host に入らなかった理由を調査します。
RFC3986を確認してみましょう。

host        = IP-literal / IPv4address / reg-name

The syntax rule for host is ambiguous because it does not completely
distinguish between an IPv4address and a reg-name.

では、 reg-name とは何か

reg-name    = *( unreserved / pct-encoded / sub-delims )

The reg-name syntax allows percent-encoded octets in order to
represent non-ASCII registered names in a uniform way that is
independent of the underlying name resolution technology.  Non-ASCII
characters must first be encoded according to UTF-8 [STD63], and then
each octet of the corresponding UTF-8 sequence must be percent-
encoded to be represented as URI characters.  URI producing
applications must not use percent-encoding in host unless it is used
to represent a UTF-8 character sequence.

引用元| https://tools.ietf.org/html/rfc3986#section-3.2.2

以上からは host が空になる理由はわかりませんでしたが、パッと見URIとはほど遠いからでしょうか。
この点についてはもう少し調査が必要です。
では、なぜ port として解釈されなかったのかについて考察していきます。
RFC3986には下記のようにあります。

port        = *DIGIT

また、

port is empty or if its value would be the same as that of the
scheme's default.

引用元| https://tools.ietf.org/html/rfc3986#section-3.2.3

つまり、 :/ はPortではないという事になります。
では問題の path について調べて行きます。
RFC3986を確認してみましょう。

こちらは全体的に重要な事が記載されているため、引用は差し控えますが、事前に目を通しておく事を推奨します。

path          = path-abempty    ; begins with "/" or is empty
              / path-absolute   ; begins with "/" but not "//"
              / path-noscheme   ; begins with a non-colon segment
              / path-rootless   ; begins with a segment
              / path-empty      ; zero characters

path-abempty  = *( "/" segment )
path-absolute = "/" [ segment-nz *( "/" segment ) ]
path-noscheme = segment-nz-nc *( "/" segment )
path-rootless = segment-nz *( "/" segment )
path-empty    = 0<pchar>

segment       = *pchar
segment-nz    = 1*pchar
segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
              ; non-zero-length segment without any colon ":"

pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"

引用元| https://tools.ietf.org/html/rfc3986#section-3.3

今問題となっている ://hostname/path はこの path-rootless に該当すると思われます。
そしてここまで書いて、Go言語の挙動も納得できました。

まとめ

RFCから関数の挙動を考察してみましたが、やはりC言語を実際に見るのが早く正確だと思われます。
私自身はC言語が得意ではないため、C言語の習得は今後の課題といったところでしょうか。
英語が苦手なところもあり、解釈が異なる所があるかもしれませんがお手柔らかにご指摘願います。

明日は@CodeDiggerMさんの『機械学習の評価指標 - ROC曲線とAUC』です。
お楽しみ下さい。

2
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
2
0