├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .testdata ├── sample-client-key.pem ├── sample-client.pem ├── sample-file.txt ├── sample-gbk.html ├── sample-image.png ├── sample-root.pem ├── sample-server-key.pem └── sample-server.pem ├── LICENSE ├── README.md ├── SECURITY.md ├── client.go ├── client_impersonate.go ├── client_test.go ├── client_wrapper.go ├── decode.go ├── decode_test.go ├── digest.go ├── dump.go ├── examples ├── README.md ├── find-popular-repo │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── opentelemetry-jaeger-tracing │ ├── README.md │ ├── github │ │ └── github.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── upload │ ├── README.md │ ├── uploadclient │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ └── uploadserver │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go └── uploadcallback │ ├── README.md │ ├── uploadclient │ ├── go.mod │ ├── go.sum │ └── main.go │ └── uploadserver │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.mod ├── go.sum ├── header.go ├── http.go ├── http2 ├── priority.go └── setting.go ├── http_request.go ├── internal ├── altsvcutil │ ├── altsvcutil.go │ └── altsvcutil_test.go ├── ascii │ ├── print.go │ └── print_test.go ├── bisect │ └── bisect.go ├── charsets │ ├── .testdata │ │ ├── HTTP-vs-UTF-8-BOM.html │ │ ├── UTF-16BE-BOM.html │ │ ├── UTF-16LE-BOM.html │ │ ├── UTF-8-BOM-vs-meta-charset.html │ │ ├── UTF-8-BOM-vs-meta-content.html │ │ ├── meta-charset-attribute.html │ │ └── meta-content-attribute.html │ ├── charsets.go │ └── charsets_test.go ├── chunked.go ├── chunked_test.go ├── common │ └── error.go ├── compress │ ├── brotli_reader.go │ ├── deflate_reader.go │ ├── gzip_reader.go │ ├── reader.go │ └── zstd_reader.go ├── dump │ └── dump.go ├── godebug │ └── godebug.go ├── godebugs │ └── table.go ├── header │ ├── header.go │ └── sort.go ├── http2 │ ├── client_conn_pool.go │ ├── databuffer.go │ ├── errors.go │ ├── flow.go │ ├── frame.go │ ├── gotrack.go │ ├── headermap.go │ ├── http2.go │ ├── pipe.go │ ├── timer.go │ ├── trace.go │ └── transport.go ├── http3 │ ├── body.go │ ├── capsule.go │ ├── client.go │ ├── conn.go │ ├── datagram.go │ ├── error.go │ ├── error_codes.go │ ├── frames.go │ ├── headers.go │ ├── http_stream.go │ ├── ip_addr.go │ ├── protocol.go │ ├── request_writer.go │ ├── server.go │ ├── state_tracking_stream.go │ ├── trace.go │ └── transport.go ├── netutil │ └── addr.go ├── quic-go │ └── quicvarint │ │ ├── io.go │ │ └── varint.go ├── socks │ ├── client.go │ ├── dial_test.go │ ├── socks.go │ └── socks_test.go ├── testcert │ └── testcert.go ├── testdata │ ├── ca.pem │ ├── cert.go │ ├── cert.pem │ ├── cert_test.go │ ├── generate_key.sh │ ├── priv.key │ └── testdata_suite_test.go ├── tests │ ├── assert.go │ ├── condition.go │ ├── file.go │ ├── net.go │ ├── reader.go │ └── transport.go ├── transport │ └── option.go └── util │ └── util.go ├── logger.go ├── logger_test.go ├── middleware.go ├── parallel_download.go ├── pkg ├── altsvc │ ├── altsvc.go │ └── jar.go └── tls │ └── conn.go ├── redirect.go ├── req.go ├── req_test.go ├── request.go ├── request_test.go ├── request_wrapper.go ├── response.go ├── retry.go ├── retry_test.go ├── roundtrip.go ├── roundtrip_js.go ├── server.go ├── textproto_reader.go ├── trace.go ├── transfer.go ├── transport.go ├── transport_default_other.go └── transport_default_wasm.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - '**.md' 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | jobs: 19 | test: 20 | strategy: 21 | matrix: 22 | go: ['1.23.x', '1.24.x'] 23 | os: [ubuntu-latest] 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | - name: Setup Go 29 | uses: actions/setup-go@v2 30 | with: 31 | go-version: ${{ matrix.go }} 32 | - name: Test 33 | run: go test ./... -coverprofile=coverage.txt 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE ignore 2 | .idea/ 3 | *.ipr 4 | *.iml 5 | *.iws 6 | .vscode/ 7 | 8 | # Emacs save files 9 | *~ 10 | \#*\# 11 | .\#* 12 | 13 | # Vim-related files 14 | [._]*.s[a-w][a-z] 15 | [._]s[a-w][a-z] 16 | *.un~ 17 | Session.vim 18 | .netrwhist 19 | 20 | # make-related metadata 21 | /.make/ 22 | 23 | # temp ignore 24 | *.log 25 | *.cache 26 | *.diff 27 | *.exe 28 | *.exe~ 29 | *.patch 30 | *.tmp 31 | *.swp 32 | 33 | # OSX trash 34 | .DS_Store -------------------------------------------------------------------------------- /.testdata/sample-client-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA779bgekwF6sVauTdq12ZB+5Say3pq0Aq43fQ8MniYPGDdhPZ 3 | PnI8pMp08tuiSAZIatyX/NozobMs/U/U6aN8Vmbf8ERfXmN5kYLN1585RObyQkwR 4 | +PS9jLUw98fuYnumPExb8hMDPB2QZIBu7oZRrzr3hdyw4/aqv9msyYi5rw/BdwA0 5 | mt9HqXZAyD23x5oKnEly5Uf/CQXF3BM8Zus05b4Nlig8oKmDPNccew3ly6JLpXHg 6 | THMyKzNmzm2Sko4JsutMEL/TV5GmOjpM0SCG62F85K+X5l0bARNwc9qtoKpzeMcz 7 | CM3g4BCdyDylup7nXyi4VAv4dJ0aPHw8Nc1rpQIDAQABAoIBAC8NQSpH15ZtjzCB 8 | ZjfBkM0LqsU6J4fieghWdX0sQe+Atqovzw0AYoJ88WLQVBMKmJ/QV0vajxOHFKdK 9 | SaDo4vgaDI0c/hKKN0ulfjx5FUY+hQEZ6NURQzogPVIDvPc7CS8AVXM25AWiT7pJ 10 | snvBhLp9OiLdYyH6QRyR3eVXngmLDtAJVz86HW0GIjWLNjApigdMjAQDCE1AlW/H 11 | HuLZbfhUZ2EtcFI98OZfWHPwLchGimIeo/WxciFrzkkkbCyvVyv8QygK0FfhMsh0 12 | SIWFZxbxx21WsDeDaX75qr+sLH2IzCjazkgDbZTdY9GsLqt8CD9YFzBxazmvH8R0 13 | de/dFGkCgYEA+er0+wF266kgdA0Js2u4szh0Nz6ljaBqzCLTcKj/mdNnc99oRJJO 14 | 1HZET4/hoIN0OTZvIH7/jx7SJSIDVOgSGpAF1wnoHhT/kjnXC6t5MDCRuceZIV76 15 | EbTAGCMwDz1yAFmTMr6qMXUcOCsBBAtlNKDtiqXshnUfctCKXlw8NP8CgYEA9ZUJ 16 | hw1/g6Bomp7VCtwk+FBQ40SRjvrMrlJWJ39/Ezbp5I9I4DfJzTjQryg5ff+Ggq0/ 17 | sUQrJsr6XF2AyQx1kV0vb5tdIlFjVNCErE/ZzVklss9r8BiSMBAIyqeMFVZWT6kd 18 | pY1/uj1Q+Y5stP9iEWq/r5mmW8N+lJnq5H3Aa1sCgYEAzySfySx9lPapn4bu83fl 19 | ryarrN6P+cNswaZb+pUYxjcjGDekBLIABLnCBPAM4y4RtxoXIaghyk6Rf5WhjU6N 20 | MtcNAB+F9OkSq/Ck/VczK24WWxXFJpPCUcqvLVJ9EySqyP91sim2hye6LBP405Fe 21 | YTDBspm0Yf3SAyg2h9+LR6ECgYBENAz+VfBZBP6oGn5+Up9t2xhr1co7FEouC63j 22 | sFQBaRnSIT0TEEtaVHIYgypcZM/dkPIEcDMvxeV8K3et3mj0YxXegB6AfmwAzRxb 23 | op2RmzWOEG8gsiI/eOSIK7oK3vx/iS8zoDWd6pOHi1eDeP2qaqQrx5ddGtEXwhtr 24 | M8VxywKBgQDUVxNDU5U+Fplr5XyxsmemnwbvzkJW0Iz04yxSyH+WRUtGnJoL6d5v 25 | fYhwL60gFFh3FFWTOiQxlvEOnhpfdqufCcO4PtHYxRMG37faBCb1ewQlcQSZ0n7i 26 | jQlLzfStlPRP8QEVBW/oc4aMDO7CVP77j5g0Wzt7Kuyh0mFYx20alg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /.testdata/sample-client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDxzCCAq+gAwIBAgIUGDq1keCeZFfm+Pp7nYCpfUyTltQwDQYJKoZIhvcNAQEL 3 | BQAwWjELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB1NpQ2h1YW4xEDAOBgNVBAcTB0No 4 | ZW5nRHUxDDAKBgNVBAoTA3JlcTELMAkGA1UECxMCQ0ExDDAKBgNVBAMTA3JlcTAg 5 | Fw0yMjAyMTAwNTEwMDBaGA8yMTIyMDExNzA1MTAwMFowXjELMAkGA1UEBhMCQ04x 6 | EDAOBgNVBAgTB1NpQ2h1YW4xEDAOBgNVBAcTB0NoZW5nZHUxDDAKBgNVBAoTA3Jl 7 | cTEMMAoGA1UECxMDcmVxMQ8wDQYDVQQDEwZjbGllbnQwggEiMA0GCSqGSIb3DQEB 8 | AQUAA4IBDwAwggEKAoIBAQDvv1uB6TAXqxVq5N2rXZkH7lJrLemrQCrjd9DwyeJg 9 | 8YN2E9k+cjykynTy26JIBkhq3Jf82jOhsyz9T9Tpo3xWZt/wRF9eY3mRgs3XnzlE 10 | 5vJCTBH49L2MtTD3x+5ie6Y8TFvyEwM8HZBkgG7uhlGvOveF3LDj9qq/2azJiLmv 11 | D8F3ADSa30epdkDIPbfHmgqcSXLlR/8JBcXcEzxm6zTlvg2WKDygqYM81xx7DeXL 12 | okulceBMczIrM2bObZKSjgmy60wQv9NXkaY6OkzRIIbrYXzkr5fmXRsBE3Bz2q2g 13 | qnN4xzMIzeDgEJ3IPKW6nudfKLhUC/h0nRo8fDw1zWulAgMBAAGjfzB9MA4GA1Ud 14 | DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T 15 | AQH/BAIwADAdBgNVHQ4EFgQUoa8OEGUk7FwiwKrw0NpjiSv9WCwwHwYDVR0jBBgw 16 | FoAUpNtNypX4RfRXBAXxI2MtrodFNMswDQYJKoZIhvcNAQELBQADggEBAAO6nZtS 17 | MPb1PSUAL7pe9ZiCL4VH+ED/RkW+JSi/nJXJisvQJqEqUCw3nkTQik6yT7+Dr8sp 18 | 0rUNwxzBNUi7ceHRsThtUcjDXN7vgZfMDU+hC3DssgfmrqtdHefkD7m4MdAJjg9F 19 | +kKOkKmCCzJ2sGMnFhVW3gQDf/PHl4VoZeBEATfqGqxmQROTJuLCpus3/yFxmsu1 20 | nT1x4K7HLzdWztxCQ/Nq/DpjD0nIMXicBFPnQamv+PHePS5NT+UEBvvbz/747aYI 21 | LG6Jczhl3oK3zjuLAwW5QSsID7CERKclZCy6BuMekAEkQeqL79T0joUUVb5ywhQz 22 | qHRB9DxZiGxazDU= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /.testdata/sample-file.txt: -------------------------------------------------------------------------------- 1 | THIS IS A SAMPLE FILE FOR TEST 2 | 3 | https://github.com/imroc/req 4 | -------------------------------------------------------------------------------- /.testdata/sample-gbk.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/req/8ef060891595f8f804a62a5b8d12a60beaa00012/.testdata/sample-gbk.html -------------------------------------------------------------------------------- /.testdata/sample-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/req/8ef060891595f8f804a62a5b8d12a60beaa00012/.testdata/sample-image.png -------------------------------------------------------------------------------- /.testdata/sample-root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDhDCCAmygAwIBAgIUfoFqa4MCUjMpXdMg7GQphA4aeDowDQYJKoZIhvcNAQEL 3 | BQAwWjELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB1NpQ2h1YW4xEDAOBgNVBAcTB0No 4 | ZW5nRHUxDDAKBgNVBAoTA3JlcTELMAkGA1UECxMCQ0ExDDAKBgNVBAMTA3JlcTAe 5 | Fw0yMjAyMTAwNDUwMDBaFw0yNzAyMDkwNDUwMDBaMFoxCzAJBgNVBAYTAkNOMRAw 6 | DgYDVQQIEwdTaUNodWFuMRAwDgYDVQQHEwdDaGVuZ0R1MQwwCgYDVQQKEwNyZXEx 7 | CzAJBgNVBAsTAkNBMQwwCgYDVQQDEwNyZXEwggEiMA0GCSqGSIb3DQEBAQUAA4IB 8 | DwAwggEKAoIBAQDrHGzHhcx+3ZyxS0BwAo+jse9587uBpAo+DseSVFPShgDNBjkc 9 | /VpdYIzXJJ5VJGv4+6zeidfh0XGElwi6J+7xJPrZu5Dx4UTD3buNIUDz7BVIhRFJ 10 | fJr9IrsFn4oYPduRK07Ij4ccOWIszdnc6Tk/2r2iEKwtqA/SEOWV/34YE+72K4vD 11 | FR/qepG6lraeCg1FvYlNEg2QDGVXc9Npc735vgh7IpXJAEOuE2hDALKOJg9233Bn 12 | qE0iSk8tXJ5NMB1r4NRnEHGMlpcZf/2ZBC1Lb9clUS3qpDzNRn0RxANoANAQ8iVG 13 | p8ysizgk9k4CnUrwPcNHkoTUvVHZbPFGbzzPAgMBAAGjQjBAMA4GA1UdDwEB/wQE 14 | AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSk203KlfhF9FcEBfEjYy2u 15 | h0U0yzANBgkqhkiG9w0BAQsFAAOCAQEAeISMnE6KNJ+g5hORssVqeY2Y8rZmA8tm 16 | NmJQX5OpmPj4M59BcmfEaurbCm4yprDHJdVJPotEUCnOmENHFkO+xhCSJHbXfT2J 17 | Vb4yuIRBT5UrkKjO/RF3Ca0tYbPgDZZtdb/7VcNfR/qGar9AYEgA8GM8D6y6m1/p 18 | L/523TV06kI9tiS6flmxJtu2ydvpimAkWwtt4sDEp7g+gB+AHt4NUnkzzH7gXB25 19 | G2cy9mtJ45ah+bX9niCOZOdSFSPKXSGCh6DqDrtKcHUK7noUu7SCH0WdX9A7KKGo 20 | 6PfnwBRh2eXJ35BgCcHE4IM/isT+v/QVt1W0hQPE1PVhSNnNIlO0og== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /.testdata/sample-server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAtAz2shdfO212Pktb//RFHXRi70utH7Xnygz9WN6yQZUg6mbs 3 | W1tFRxUo2JOKm/feTGJ6VohFEIdv7kMTVFCpwSDe31sz9yM1l2D85adJXQuOpiMV 4 | ux264RnyTJIVLhkFVvHyGohWYAwrMGoeV+8lBzTnbhnk0OHujJCdw1iLohc3dodE 5 | JNRXxOB6LZ/3kj1qo1IjQwlRpDXuGRRBNO8GBW8G72QaYTamRP0bvByJ09nuWlxZ 6 | V2ouCLYXRAhbx3BXCj1zEiqEQBAEOhQrooPNHqgrsYapvyVHmWJFDOZaIkGNfHKn 7 | qPtM8RhsD0yqKtmY38voQaowfHKKS6F52JYP+QIDAQABAoIBAHtGUPXYaLygko/O 8 | OvxA+71R/ZcHgk4u1reRMzjQqM2cVEAJHhTipckoZKH8Sq/FAu/bkRWEEX1irbE9 9 | PZPB8qgnYFEe+bJg6gVuQ1jds65ABngbl3pYvaX3hN0GO/gm62//EZs286SpUDzC 10 | u2nLc9e+UiIhGngl6JVXQp0IF/pusWsnKN34QK81qRjCEInlPLI04o4Imy/3a/zc 11 | a4DwCwMqXbYHyv97nufApQB/1qYjiKcrhhk9AeRr3vZyOFrYec2avQKuWhLrGpRN 12 | koNRIPKrgTKRq3gsSmCiPiSRUD40pmtJujhAJqvcZsqSqyv+J2yPdjjTAyqyV/29 13 | M98GwoECgYEAwf75kTv+k4INdnJVvMXdWFcxkgzq8/fLmBOGoTNzKj1fHTDZFryD 14 | X41ymNBee26YaUMTMg7NahaaT8s+bk7LKjYFODhD6VVdvfagVchbkB5wK27Cwa0z 15 | XzExLFtnUlE1o/ciZwqUlzLxRXQKlfTHCfiALqBmMIEDK0CRCtxY2rECgYEA7Zj1 16 | ynolm5jc84yUiHhCKUUVaL0mP9IFV5Dydo+UymzJmjMUx1H7baFbHo2rZ6plkRop 17 | AjH5LZ8cJ06EgGu9rOlHwwKYIA2FKnzwCiFE9nH8BE+ki2mBNAwsj+NtCBTvEBsN 18 | b3byhHMMdrJj6CPgaZZOCAHHp3kBKjNmL4Hoy8kCgYEArrqM5jb3MLzui0Sn3IMK 19 | vkqqpzVjWaJSigLsO70veVgVlyEsJsJcQXARS3pB30LZm9WCMJAMjAUXr88LyCbH 20 | 7pkBUoW7BSqSaEr+VsVDUydXOIdmezMZFiAkfiNFiGsEuU4aelyZQSXtEfVWo4H4 21 | 1A4yxcxKvl01EXvyJ6oXjcECgYA8JTJjNRR8FPApvvaCrV6iL9jBkNAz66hqiEi4 22 | dpRFwdAu9qtV4YzyLZxxWY+ASIQ5fRPQeHIJeHOaB6hHEf8L3GnMFcYIpyOEo+fn 23 | yJA6ipQvSzHuEKEiWcqWCg45s4Lo4tA93TB7Etye132u8BYI5IGQSVMPM/R1iFlf 24 | wVT68QKBgAxY1euChqV+Wio74IaNiHsnk6KzLiUaBOV5i1xA1Kz81mownYI1n9jk 25 | LXTzgTrmJ8jtL0HmaJcl7plIre8h3WAQhHFXWjPhmE+YhwVfPU77JgA1o5Pn7KKm 26 | NDoSb3GDRYHuBaAK5SvBxQ9re6rkueK+N5cdcB7ozt1h1FLFz0/D 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /.testdata/sample-server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID6DCCAtCgAwIBAgIUPpYORWb7lrYrrfmFRDrugeHzncUwDQYJKoZIhvcNAQEL 3 | BQAwWjELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB1NpQ2h1YW4xEDAOBgNVBAcTB0No 4 | ZW5nRHUxDDAKBgNVBAoTA3JlcTELMAkGA1UECxMCQ0ExDDAKBgNVBAMTA3JlcTAg 5 | Fw0yMjAyMTAwNTA0MDBaGA8yMTIyMDExNzA1MDQwMFowXjELMAkGA1UEBhMCQ04x 6 | EDAOBgNVBAgTB1NpQ2h1YW4xEDAOBgNVBAcTB0NoZW5nZHUxDDAKBgNVBAoTA3Jl 7 | cTEMMAoGA1UECxMDcmVxMQ8wDQYDVQQDEwZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEB 8 | AQUAA4IBDwAwggEKAoIBAQC0DPayF187bXY+S1v/9EUddGLvS60ftefKDP1Y3rJB 9 | lSDqZuxbW0VHFSjYk4qb995MYnpWiEUQh2/uQxNUUKnBIN7fWzP3IzWXYPzlp0ld 10 | C46mIxW7HbrhGfJMkhUuGQVW8fIaiFZgDCswah5X7yUHNOduGeTQ4e6MkJ3DWIui 11 | Fzd2h0Qk1FfE4Hotn/eSPWqjUiNDCVGkNe4ZFEE07wYFbwbvZBphNqZE/Ru8HInT 12 | 2e5aXFlXai4IthdECFvHcFcKPXMSKoRAEAQ6FCuig80eqCuxhqm/JUeZYkUM5loi 13 | QY18cqeo+0zxGGwPTKoq2Zjfy+hBqjB8copLoXnYlg/5AgMBAAGjgZ8wgZwwDgYD 14 | VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV 15 | HRMBAf8EAjAAMB0GA1UdDgQWBBSjaTM5PsU7WKjGqHjLG1ShTukoljAfBgNVHSME 16 | GDAWgBSk203KlfhF9FcEBfEjYy2uh0U0yzAdBgNVHREEFjAUgglsb2NhbGhvc3SH 17 | BH8AAAGGASowDQYJKoZIhvcNAQELBQADggEBAMr4rw7MAwCIJNuFxHukWIxIyq6B 18 | g2P2kFIU+oWVhKd0VlHZ/lrjR3eB1GJ4lC8n+yslEYA0nipEBZm1zABUKNhRhmam 19 | Oi8Gf09yOg5+NZM7BihgK+AF1Kc2a282XpQOHqqqr20QAeO9RLBwBjxc4koxgPog 20 | HSVgbNceemxFrfT8kzjyjv9SRpeRjeAYLILHxPABRVEuO5rOMoRhZOGMxb65IAuT 21 | aFbOPIdBsW1d2/cx5hQT1yfXXOjFXvKVL3pYkEDq+61E40G8Sfr1ZqLnQs6+Fhiy 22 | vUsHXX7yq6hnyrhVy1wTL0mLqadK+umEybkalnCMHVlusNnLXuf43k9xlFU= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2022 roc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | req version >= `v3.43.x` 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Email: roc@imroc.cc 10 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "github.com/imroc/req/v3/internal/charsets" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | var textContentTypes = []string{"text", "json", "xml", "html", "java"} 10 | 11 | var autoDecodeText = autoDecodeContentTypeFunc(textContentTypes...) 12 | 13 | func autoDecodeContentTypeFunc(contentTypes ...string) func(contentType string) bool { 14 | return func(contentType string) bool { 15 | for _, ct := range contentTypes { 16 | if strings.Contains(contentType, ct) { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | } 23 | 24 | type decodeReaderCloser struct { 25 | io.ReadCloser 26 | decodeReader io.Reader 27 | } 28 | 29 | func (d *decodeReaderCloser) Read(p []byte) (n int, err error) { 30 | return d.decodeReader.Read(p) 31 | } 32 | 33 | func newAutoDecodeReadCloser(input io.ReadCloser, t *Transport) *autoDecodeReadCloser { 34 | return &autoDecodeReadCloser{ReadCloser: input, t: t} 35 | } 36 | 37 | type autoDecodeReadCloser struct { 38 | io.ReadCloser 39 | t *Transport 40 | decodeReader io.Reader 41 | detected bool 42 | peek []byte 43 | } 44 | 45 | func (a *autoDecodeReadCloser) peekRead(p []byte) (n int, err error) { 46 | n, err = a.ReadCloser.Read(p) 47 | if n == 0 || (err != nil && err != io.EOF) { 48 | return 49 | } 50 | a.detected = true 51 | enc, name := charsets.FindEncoding(p) 52 | if enc == nil { 53 | return 54 | } 55 | if a.t.Debugf != nil { 56 | a.t.Debugf("charset %s found in body's meta, auto-decode to utf-8", name) 57 | } 58 | dc := enc.NewDecoder() 59 | a.decodeReader = dc.Reader(a.ReadCloser) 60 | var pp []byte 61 | pp, err = dc.Bytes(p[:n]) 62 | if err != nil { 63 | return 64 | } 65 | if len(pp) > len(p) { 66 | a.peek = make([]byte, len(pp)-len(p)) 67 | copy(a.peek, pp[len(p):]) 68 | copy(p, pp[:len(p)]) 69 | n = len(p) 70 | return 71 | } 72 | copy(p, pp) 73 | n = len(p) 74 | return 75 | } 76 | 77 | func (a *autoDecodeReadCloser) peekDrain(p []byte) (n int, err error) { 78 | if len(a.peek) > len(p) { 79 | copy(p, a.peek[:len(p)]) 80 | peek := make([]byte, len(a.peek)-len(p)) 81 | copy(peek, a.peek[len(p):]) 82 | a.peek = peek 83 | n = len(p) 84 | return 85 | } 86 | if len(a.peek) == len(p) { 87 | copy(p, a.peek) 88 | n = len(p) 89 | a.peek = nil 90 | return 91 | } 92 | pp := make([]byte, len(p)-len(a.peek)) 93 | nn, err := a.decodeReader.Read(pp) 94 | n = len(a.peek) + nn 95 | copy(p[:len(a.peek)], a.peek) 96 | copy(p[len(a.peek):], pp[:nn]) 97 | a.peek = nil 98 | return 99 | } 100 | 101 | func (a *autoDecodeReadCloser) Read(p []byte) (n int, err error) { 102 | if !a.detected { 103 | return a.peekRead(p) 104 | } 105 | if a.peek != nil { 106 | return a.peekDrain(p) 107 | } 108 | if a.decodeReader != nil { 109 | return a.decodeReader.Read(p) 110 | } 111 | return a.ReadCloser.Read(p) // can not determine charset, not decode 112 | } 113 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "github.com/imroc/req/v3/internal/tests" 5 | "testing" 6 | ) 7 | 8 | func TestPeekDrain(t *testing.T) { 9 | a := autoDecodeReadCloser{peek: []byte("test")} 10 | p := make([]byte, 2) 11 | n, _ := a.peekDrain(p) 12 | tests.AssertEqual(t, 2, n) 13 | tests.AssertEqual(t, true, a.peek != nil) 14 | n, _ = a.peekDrain(p) 15 | tests.AssertEqual(t, 2, n) 16 | tests.AssertEqual(t, true, a.peek == nil) 17 | } 18 | -------------------------------------------------------------------------------- /dump.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "github.com/imroc/req/v3/internal/dump" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // DumpOptions controls the dump behavior. 10 | type DumpOptions struct { 11 | Output io.Writer 12 | RequestOutput io.Writer 13 | ResponseOutput io.Writer 14 | RequestHeaderOutput io.Writer 15 | RequestBodyOutput io.Writer 16 | ResponseHeaderOutput io.Writer 17 | ResponseBodyOutput io.Writer 18 | RequestHeader bool 19 | RequestBody bool 20 | ResponseHeader bool 21 | ResponseBody bool 22 | Async bool 23 | } 24 | 25 | // Clone return a copy of DumpOptions 26 | func (do *DumpOptions) Clone() *DumpOptions { 27 | if do == nil { 28 | return nil 29 | } 30 | d := *do 31 | return &d 32 | } 33 | 34 | type dumpOptions struct { 35 | *DumpOptions 36 | } 37 | 38 | func (o dumpOptions) Output() io.Writer { 39 | if o.DumpOptions.Output == nil { 40 | return os.Stdout 41 | } 42 | return o.DumpOptions.Output 43 | } 44 | 45 | func (o dumpOptions) RequestHeaderOutput() io.Writer { 46 | if o.DumpOptions.RequestHeaderOutput != nil { 47 | return o.DumpOptions.RequestHeaderOutput 48 | } 49 | if o.DumpOptions.RequestOutput != nil { 50 | return o.DumpOptions.RequestOutput 51 | } 52 | return o.Output() 53 | } 54 | 55 | func (o dumpOptions) RequestBodyOutput() io.Writer { 56 | if o.DumpOptions.RequestBodyOutput != nil { 57 | return o.DumpOptions.RequestBodyOutput 58 | } 59 | if o.DumpOptions.RequestOutput != nil { 60 | return o.DumpOptions.RequestOutput 61 | } 62 | return o.Output() 63 | } 64 | 65 | func (o dumpOptions) ResponseHeaderOutput() io.Writer { 66 | if o.DumpOptions.ResponseHeaderOutput != nil { 67 | return o.DumpOptions.ResponseHeaderOutput 68 | } 69 | if o.DumpOptions.ResponseOutput != nil { 70 | return o.DumpOptions.ResponseOutput 71 | } 72 | return o.Output() 73 | } 74 | 75 | func (o dumpOptions) ResponseBodyOutput() io.Writer { 76 | if o.DumpOptions.ResponseBodyOutput != nil { 77 | return o.DumpOptions.ResponseBodyOutput 78 | } 79 | if o.DumpOptions.ResponseOutput != nil { 80 | return o.DumpOptions.ResponseOutput 81 | } 82 | return o.Output() 83 | } 84 | 85 | func (o dumpOptions) RequestHeader() bool { 86 | return o.DumpOptions.RequestHeader 87 | } 88 | 89 | func (o dumpOptions) RequestBody() bool { 90 | return o.DumpOptions.RequestBody 91 | } 92 | 93 | func (o dumpOptions) ResponseHeader() bool { 94 | return o.DumpOptions.ResponseHeader 95 | } 96 | 97 | func (o dumpOptions) ResponseBody() bool { 98 | return o.DumpOptions.ResponseBody 99 | } 100 | 101 | func (o dumpOptions) Async() bool { 102 | return o.DumpOptions.Async 103 | } 104 | 105 | func (o dumpOptions) Clone() dump.Options { 106 | return dumpOptions{o.DumpOptions.Clone()} 107 | } 108 | 109 | func newDefaultDumpOptions() *DumpOptions { 110 | return &DumpOptions{ 111 | Output: os.Stdout, 112 | RequestBody: true, 113 | ResponseBody: true, 114 | ResponseHeader: true, 115 | RequestHeader: true, 116 | } 117 | } 118 | 119 | func newDumper(opt *DumpOptions) *dump.Dumper { 120 | if opt == nil { 121 | opt = newDefaultDumpOptions() 122 | } 123 | if opt.Output == nil { 124 | opt.Output = os.Stderr 125 | } 126 | return dump.NewDumper(dumpOptions{opt}) 127 | } 128 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # examples 2 | 3 | * [find-popular-repo](find-popular-repo): Invoke github api to find someone's most popular repo. 4 | * [upload](upload): Use `req` to upload files. Contains a server written with `gin` and a client written with `req` -------------------------------------------------------------------------------- /examples/find-popular-repo/README.md: -------------------------------------------------------------------------------- 1 | # find-popular-repo 2 | 3 | This is a runable example of req, using the Github API [List repositories for a user](https://docs.github.com/cn/rest/reference/repos#list-repositories-for-a-user) to find someone's the most popular github repo. 4 | 5 | ## How to run 6 | 7 | ```bash 8 | go run . 9 | ``` 10 | 11 | ## Modify it 12 | 13 | Change the global `username` vairable to your own github username: 14 | 15 | ```go 16 | var username = "imroc" 17 | ``` -------------------------------------------------------------------------------- /examples/find-popular-repo/go.mod: -------------------------------------------------------------------------------- 1 | module find-popular-repo 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.3 6 | 7 | replace github.com/imroc/req/v3 => ../../ 8 | 9 | require github.com/imroc/req/v3 v3.0.0 10 | 11 | require ( 12 | github.com/cheekybits/genny v1.0.0 // indirect 13 | github.com/fsnotify/fsnotify v1.5.4 // indirect 14 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/marten-seemann/qpack v0.2.1 // indirect 18 | github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect 19 | github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect 20 | github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect 21 | github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect 22 | github.com/nxadm/tail v1.4.8 // indirect 23 | github.com/onsi/ginkgo v1.16.5 // indirect 24 | golang.org/x/crypto v0.29.0 // indirect 25 | golang.org/x/mod v0.22.0 // indirect 26 | golang.org/x/net v0.31.0 // indirect 27 | golang.org/x/sys v0.27.0 // indirect 28 | golang.org/x/text v0.20.0 // indirect 29 | golang.org/x/tools v0.27.0 // indirect 30 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /examples/find-popular-repo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/imroc/req/v3" 8 | ) 9 | 10 | // Change the name if you want 11 | var username = "imroc" 12 | 13 | func main() { 14 | repo, star, err := findTheMostPopularRepo(username) 15 | if err != nil { 16 | fmt.Println(err) 17 | return 18 | } 19 | fmt.Printf("The most popular repo of %s is %s, which have %d stars\n", username, repo, star) 20 | } 21 | 22 | func init() { 23 | req.EnableDebugLog(). 24 | EnableTraceAll(). 25 | EnableDumpEachRequest(). 26 | SetCommonErrorResult(&ErrorMessage{}). 27 | OnAfterResponse(func(client *req.Client, resp *req.Response) error { 28 | if resp.Err != nil { 29 | return nil 30 | } 31 | if errMsg, ok := resp.ErrorResult().(*ErrorMessage); ok { 32 | resp.Err = errMsg 33 | return nil 34 | } 35 | if !resp.IsSuccessState() { 36 | resp.Err = fmt.Errorf("bad status: %s\nraw content:\n%s", resp.Status, resp.Dump()) 37 | } 38 | return nil 39 | }) 40 | } 41 | 42 | type Repo struct { 43 | Name string `json:"name"` 44 | Star int `json:"stargazers_count"` 45 | } 46 | type ErrorMessage struct { 47 | Message string `json:"message"` 48 | } 49 | 50 | func (msg *ErrorMessage) Error() string { 51 | return fmt.Sprintf("API Error: %s", msg.Message) 52 | } 53 | 54 | func findTheMostPopularRepo(username string) (repo string, star int, err error) { 55 | var popularRepo Repo 56 | var resp *req.Response 57 | 58 | for page := 1; ; page++ { 59 | repos := []*Repo{} 60 | resp, err = req.SetHeader("Accept", "application/vnd.github.v3+json"). 61 | SetQueryParams(map[string]string{ 62 | "type": "owner", 63 | "page": strconv.Itoa(page), 64 | "per_page": "100", 65 | "sort": "updated", 66 | "direction": "desc", 67 | }). 68 | SetPathParam("username", username). 69 | SetSuccessResult(&repos). 70 | Get("https://api.github.com/users/{username}/repos") 71 | 72 | fmt.Println("TraceInfo:") 73 | fmt.Println("----------") 74 | fmt.Println(resp.TraceInfo()) 75 | fmt.Println() 76 | 77 | if err != nil { 78 | return 79 | } 80 | 81 | if !resp.IsSuccessState() { // HTTP status `code >= 200 and <= 299` is considered as success by default 82 | return 83 | } 84 | for _, repo := range repos { 85 | if repo.Star >= popularRepo.Star { 86 | popularRepo = *repo 87 | } 88 | } 89 | if len(repo) == 100 { // Try Next page 90 | continue 91 | } 92 | // All repos have been traversed, return the final result 93 | repo = popularRepo.Name 94 | star = popularRepo.Star 95 | return 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/opentelemetry-jaeger-tracing/README.md: -------------------------------------------------------------------------------- 1 | # opentelemetry-jaeger-tracing 2 | 3 | This is a runnable example of req, which uses the built-in tiny github sdk built on req to query and display the information of the specified user. 4 | 5 | Best of all, it integrates seamlessly with jaeger tracing and is very easy to extend. 6 | 7 | ## How to run 8 | 9 | First, use `docker` or `podman` to start a test jeager container (see jeager official doc: [ Getting Started](https://www.jaegertracing.io/docs/1.37/getting-started/#all-in-one)). 10 | 11 | Then, run example: 12 | 13 | ```bash 14 | go run . 15 | ``` 16 | ```txt 17 | Please give a github username: 18 | ``` 19 | 20 | Input a github username, e.g. `imroc`: 21 | 22 | ```bash 23 | $ go run . 24 | Please give a github username: imroc 25 | The moust popular repo of roc (https://imroc.cc) is req, which have 2500 stars 26 | ``` 27 | 28 | Then enter the Jaeger UI with browser (`http://127.0.0.1:16686/`), checkout the tracing details. 29 | 30 | Run example again, try to input some username that doesn't exist, and check the error log in Jaeger UI. 31 | -------------------------------------------------------------------------------- /examples/opentelemetry-jaeger-tracing/go.mod: -------------------------------------------------------------------------------- 1 | module opentelemetry-jaeger-tracing 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.3 6 | 7 | replace github.com/imroc/req/v3 => ../../ 8 | 9 | require ( 10 | github.com/imroc/req/v3 v3.0.0 11 | go.opentelemetry.io/otel v1.9.0 12 | go.opentelemetry.io/otel/exporters/jaeger v1.9.0 13 | go.opentelemetry.io/otel/sdk v1.9.0 14 | go.opentelemetry.io/otel/trace v1.9.0 15 | ) 16 | 17 | require ( 18 | github.com/cheekybits/genny v1.0.0 // indirect 19 | github.com/fsnotify/fsnotify v1.5.4 // indirect 20 | github.com/go-logr/logr v1.2.3 // indirect 21 | github.com/go-logr/stdr v1.2.2 // indirect 22 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 23 | github.com/hashicorp/errwrap v1.1.0 // indirect 24 | github.com/hashicorp/go-multierror v1.1.1 // indirect 25 | github.com/lucas-clemente/quic-go v0.28.1 // indirect 26 | github.com/marten-seemann/qpack v0.2.1 // indirect 27 | github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect 28 | github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect 29 | github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect 30 | github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect 31 | github.com/nxadm/tail v1.4.8 // indirect 32 | github.com/onsi/ginkgo v1.16.5 // indirect 33 | golang.org/x/crypto v0.29.0 // indirect 34 | golang.org/x/mod v0.22.0 // indirect 35 | golang.org/x/net v0.31.0 // indirect 36 | golang.org/x/sys v0.27.0 // indirect 37 | golang.org/x/text v0.20.0 // indirect 38 | golang.org/x/tools v0.27.0 // indirect 39 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /examples/opentelemetry-jaeger-tracing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go.opentelemetry.io/otel" 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/codes" 9 | "go.opentelemetry.io/otel/exporters/jaeger" 10 | "go.opentelemetry.io/otel/sdk/resource" 11 | "go.opentelemetry.io/otel/sdk/trace" 12 | semconv "go.opentelemetry.io/otel/semconv/v1.12.0" 13 | "log" 14 | "opentelemetry-jaeger-tracing/github" 15 | "os" 16 | "os/signal" 17 | "syscall" 18 | ) 19 | 20 | const serviceName = "github-query" 21 | 22 | var githubClient *github.Client 23 | 24 | func traceProvider() (*trace.TracerProvider, error) { 25 | // Create the Jaeger exporter 26 | ep := os.Getenv("JAEGER_ENDPOINT") 27 | if ep == "" { 28 | ep = "http://localhost:14268/api/traces" 29 | } 30 | exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(ep))) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | // Record information about this application in a Resource. 36 | res, _ := resource.Merge( 37 | resource.Default(), 38 | resource.NewWithAttributes( 39 | semconv.SchemaURL, 40 | semconv.ServiceNameKey.String(serviceName), 41 | semconv.ServiceVersionKey.String("v0.1.0"), 42 | attribute.String("environment", "test"), 43 | ), 44 | ) 45 | 46 | // Create the TraceProvider. 47 | tp := trace.NewTracerProvider( 48 | // Always be sure to batch in production. 49 | trace.WithBatcher(exp), 50 | // Record information about this application in a Resource. 51 | trace.WithResource(res), 52 | trace.WithSampler(trace.AlwaysSample()), 53 | ) 54 | return tp, nil 55 | } 56 | 57 | // QueryUser queries information for specified GitHub user, and display a 58 | // brief introduction which includes name, blog, and the most popular repo. 59 | func QueryUser(username string) error { 60 | ctx, span := otel.Tracer("query").Start(context.Background(), "QueryUser") 61 | defer span.End() 62 | 63 | span.SetAttributes( 64 | attribute.String("query.username", username), 65 | ) 66 | profile, err := githubClient.GetUserProfile(ctx, username) 67 | if err != nil { 68 | span.RecordError(err) 69 | span.SetStatus(codes.Error, err.Error()) 70 | return err 71 | } 72 | span.SetAttributes( 73 | attribute.String("query.name", profile.Name), 74 | attribute.String("result.blog", profile.Blog), 75 | ) 76 | repo, err := findMostPopularRepo(ctx, username) 77 | if err != nil { 78 | span.RecordError(err) 79 | span.SetStatus(codes.Error, err.Error()) 80 | return err 81 | } 82 | span.SetAttributes( 83 | attribute.String("popular.repo.name", repo.Name), 84 | attribute.Int("popular.repo.star", repo.Star), 85 | ) 86 | fmt.Printf("The most popular repo of %s (%s) is %s, with %d stars\n", profile.Name, profile.Blog, repo.Name, repo.Star) 87 | return nil 88 | } 89 | 90 | func findMostPopularRepo(ctx context.Context, username string) (repo *github.Repo, err error) { 91 | ctx, span := otel.Tracer("query").Start(ctx, "findMostPopularRepo") 92 | defer span.End() 93 | 94 | for page := 1; ; page++ { 95 | var repos []*github.Repo 96 | repos, err = githubClient.ListUserRepo(ctx, username, page) 97 | if err != nil { 98 | return 99 | } 100 | if len(repos) == 0 { 101 | break 102 | } 103 | if repo == nil { 104 | repo = repos[0] 105 | } 106 | for _, rp := range repos[1:] { 107 | if rp.Star >= repo.Star { 108 | repo = rp 109 | } 110 | } 111 | if len(repos) == 100 { 112 | continue 113 | } 114 | break 115 | } 116 | 117 | if repo == nil { 118 | err = fmt.Errorf("no repo found for %s", username) 119 | } 120 | return 121 | } 122 | 123 | func main() { 124 | tp, err := traceProvider() 125 | if err != nil { 126 | panic(err) 127 | } 128 | otel.SetTracerProvider(tp) 129 | 130 | githubClient = github.NewClient() 131 | if os.Getenv("DEBUG") == "on" { 132 | githubClient.SetDebug(true) 133 | } 134 | if token := os.Getenv("GITHUB_TOKEN"); token != "" { 135 | githubClient.LoginWithToken(token) 136 | } 137 | githubClient.SetTracer(otel.Tracer("github")) 138 | 139 | sigs := make(chan os.Signal, 1) 140 | signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) 141 | go func() { 142 | sig := <-sigs 143 | fmt.Printf("Caught %s, shutting down\n", sig) 144 | if err := tp.Shutdown(context.Background()); err != nil { 145 | log.Fatal(err) 146 | } 147 | os.Exit(0) 148 | }() 149 | 150 | for { 151 | var name string 152 | fmt.Printf("Please give a github username: ") 153 | _, err := fmt.Fscanf(os.Stdin, "%s\n", &name) 154 | if err != nil { 155 | panic(err) 156 | } 157 | err = QueryUser(name) 158 | if err != nil { 159 | fmt.Println(err.Error()) 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /examples/upload/README.md: -------------------------------------------------------------------------------- 1 | # upload 2 | 3 | This is a upload exmaple for `req` 4 | 5 | ## How to Run 6 | 7 | Run `uploadserver`: 8 | 9 | ```go 10 | cd uploadserver 11 | go run . 12 | ``` 13 | 14 | Run `uploadclient`: 15 | 16 | ```go 17 | cd uploadclient 18 | go run . 19 | ``` -------------------------------------------------------------------------------- /examples/upload/uploadclient/go.mod: -------------------------------------------------------------------------------- 1 | module uploadclient 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.3 6 | 7 | replace github.com/imroc/req/v3 => ../../../ 8 | 9 | require github.com/imroc/req/v3 v3.0.0 10 | 11 | require ( 12 | github.com/cheekybits/genny v1.0.0 // indirect 13 | github.com/fsnotify/fsnotify v1.5.4 // indirect 14 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/lucas-clemente/quic-go v0.28.1 // indirect 18 | github.com/marten-seemann/qpack v0.2.1 // indirect 19 | github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect 20 | github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect 21 | github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect 22 | github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect 23 | github.com/nxadm/tail v1.4.8 // indirect 24 | github.com/onsi/ginkgo v1.16.5 // indirect 25 | golang.org/x/crypto v0.29.0 // indirect 26 | golang.org/x/mod v0.22.0 // indirect 27 | golang.org/x/net v0.31.0 // indirect 28 | golang.org/x/sys v0.27.0 // indirect 29 | golang.org/x/text v0.20.0 // indirect 30 | golang.org/x/tools v0.27.0 // indirect 31 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /examples/upload/uploadclient/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/imroc/req/v3" 5 | ) 6 | 7 | func main() { 8 | req.EnableDumpAllWithoutRequestBody() 9 | req.SetFile("files", "../../../README.md"). 10 | SetFile("files", "../../../LICENSE"). 11 | SetFormData(map[string]string{ 12 | "name": "imroc", 13 | "email": "roc@imroc.cc", 14 | }). 15 | Post("http://127.0.0.1:8888/upload") 16 | /* Output 17 | POST /upload HTTP/1.1 18 | Host: 127.0.0.1:8888 19 | User-Agent: req/v2 (https://github.com/imroc/req) 20 | Transfer-Encoding: chunked 21 | Content-Type: multipart/form-data; boundary=6af1b071a682709355cf5fb15b9cf9e793df7a45e5cd1eb7c413f2e72bf6 22 | Accept-Encoding: gzip 23 | 24 | HTTP/1.1 200 OK 25 | Content-Type: text/plain; charset=utf-8 26 | Date: Tue, 25 Jan 2022 09:40:36 GMT 27 | Content-Length: 76 28 | 29 | Uploaded successfully 2 files with fields name=imroc and email=roc@imroc.cc. 30 | */ 31 | } 32 | -------------------------------------------------------------------------------- /examples/upload/uploadserver/go.mod: -------------------------------------------------------------------------------- 1 | module uploadserver 2 | 3 | go 1.18 4 | 5 | require github.com/gin-gonic/gin v1.8.1 6 | 7 | require ( 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/go-playground/locales v0.14.0 // indirect 10 | github.com/go-playground/universal-translator v0.18.0 // indirect 11 | github.com/go-playground/validator/v10 v10.11.0 // indirect 12 | github.com/goccy/go-json v0.9.10 // indirect 13 | github.com/golang/protobuf v1.5.2 // indirect 14 | github.com/json-iterator/go v1.1.12 // indirect 15 | github.com/leodido/go-urn v1.2.1 // indirect 16 | github.com/mattn/go-isatty v0.0.14 // indirect 17 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 18 | github.com/modern-go/reflect2 v1.0.2 // indirect 19 | github.com/pelletier/go-toml/v2 v2.0.2 // indirect 20 | github.com/ugorji/go/codec v1.2.7 // indirect 21 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 22 | golang.org/x/net v0.0.0-20220809012201-f428fae20770 // indirect 23 | golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect 24 | golang.org/x/text v0.3.7 // indirect 25 | google.golang.org/protobuf v1.28.1 // indirect 26 | gopkg.in/yaml.v2 v2.4.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /examples/upload/uploadserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "path/filepath" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | router := gin.Default() 12 | router.POST("/upload", func(c *gin.Context) { 13 | name := c.PostForm("name") 14 | email := c.PostForm("email") 15 | 16 | // Multipart form 17 | form, err := c.MultipartForm() 18 | if err != nil { 19 | c.String(http.StatusBadRequest, "get form err: %s", err.Error()) 20 | return 21 | } 22 | files := form.File["files"] 23 | 24 | for _, file := range files { 25 | filename := filepath.Base(file.Filename) 26 | if err := c.SaveUploadedFile(file, filename); err != nil { 27 | c.String(http.StatusBadRequest, "upload file err: %s", err.Error()) 28 | return 29 | } 30 | } 31 | 32 | c.String(http.StatusOK, "Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email) 33 | }) 34 | router.Run(":8888") 35 | } 36 | -------------------------------------------------------------------------------- /examples/uploadcallback/README.md: -------------------------------------------------------------------------------- 1 | # uploadcallback 2 | 3 | This is a upload callback exmaple for `req` 4 | 5 | ## How to Run 6 | 7 | Run `uploadserver`: 8 | 9 | ```go 10 | cd uploadserver 11 | go run . 12 | ``` 13 | 14 | Run `uploadclient`: 15 | 16 | ```go 17 | cd uploadclient 18 | go run . 19 | ``` -------------------------------------------------------------------------------- /examples/uploadcallback/uploadclient/go.mod: -------------------------------------------------------------------------------- 1 | module uploadclient 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.3 6 | 7 | replace github.com/imroc/req/v3 => ../../../ 8 | 9 | require github.com/imroc/req/v3 v3.0.0 10 | 11 | require ( 12 | github.com/cheekybits/genny v1.0.0 // indirect 13 | github.com/fsnotify/fsnotify v1.5.4 // indirect 14 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/lucas-clemente/quic-go v0.28.1 // indirect 18 | github.com/marten-seemann/qpack v0.2.1 // indirect 19 | github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect 20 | github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect 21 | github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect 22 | github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect 23 | github.com/nxadm/tail v1.4.8 // indirect 24 | github.com/onsi/ginkgo v1.16.5 // indirect 25 | golang.org/x/crypto v0.29.0 // indirect 26 | golang.org/x/mod v0.22.0 // indirect 27 | golang.org/x/net v0.31.0 // indirect 28 | golang.org/x/sys v0.27.0 // indirect 29 | golang.org/x/text v0.20.0 // indirect 30 | golang.org/x/tools v0.27.0 // indirect 31 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /examples/uploadcallback/uploadclient/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | "github.com/imroc/req/v3" 8 | ) 9 | 10 | type SlowReader struct { 11 | Size int 12 | n int 13 | } 14 | 15 | func (r *SlowReader) Close() error { 16 | return nil 17 | } 18 | 19 | func (r *SlowReader) Read(p []byte) (int, error) { 20 | if r.n >= r.Size { 21 | return 0, io.EOF 22 | } 23 | time.Sleep(1 * time.Millisecond) 24 | n := len(p) 25 | if r.n+n >= r.Size { 26 | n = r.Size - r.n 27 | } 28 | for i := 0; i < n; i++ { 29 | p[i] = 'h' 30 | } 31 | r.n += n 32 | return n, nil 33 | } 34 | 35 | func main() { 36 | size := 10 * 1024 * 1024 37 | req.SetFileUpload(req.FileUpload{ 38 | ParamName: "file", 39 | FileName: "test.txt", 40 | GetFileContent: func() (io.ReadCloser, error) { 41 | return &SlowReader{Size: size}, nil 42 | }, 43 | FileSize: int64(size), 44 | }).SetUploadCallbackWithInterval(func(info req.UploadInfo) { 45 | fmt.Printf("%s: %.2f%%\n", info.FileName, float64(info.UploadedSize)/float64(info.FileSize)*100.0) 46 | }, 30*time.Millisecond).Post("http://127.0.0.1:8888/upload") 47 | } 48 | -------------------------------------------------------------------------------- /examples/uploadcallback/uploadserver/go.mod: -------------------------------------------------------------------------------- 1 | module uploadserver 2 | 3 | go 1.18 4 | 5 | require github.com/gin-gonic/gin v1.8.1 6 | 7 | require ( 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/go-playground/locales v0.14.0 // indirect 10 | github.com/go-playground/universal-translator v0.18.0 // indirect 11 | github.com/go-playground/validator/v10 v10.11.0 // indirect 12 | github.com/goccy/go-json v0.9.10 // indirect 13 | github.com/golang/protobuf v1.5.2 // indirect 14 | github.com/json-iterator/go v1.1.12 // indirect 15 | github.com/leodido/go-urn v1.2.1 // indirect 16 | github.com/mattn/go-isatty v0.0.14 // indirect 17 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 18 | github.com/modern-go/reflect2 v1.0.2 // indirect 19 | github.com/pelletier/go-toml/v2 v2.0.2 // indirect 20 | github.com/ugorji/go/codec v1.2.7 // indirect 21 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 22 | golang.org/x/net v0.0.0-20220809012201-f428fae20770 // indirect 23 | golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect 24 | golang.org/x/text v0.3.7 // indirect 25 | google.golang.org/protobuf v1.28.1 // indirect 26 | gopkg.in/yaml.v2 v2.4.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /examples/uploadcallback/uploadserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | router := gin.Default() 11 | router.POST("/upload", func(c *gin.Context) { 12 | body := c.Request.Body 13 | io.Copy(io.Discard, body) 14 | c.String(http.StatusOK, "ok") 15 | }) 16 | router.Run(":8888") 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/imroc/req/v3 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/andybalholm/brotli v1.1.1 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/icholy/digest v1.1.0 9 | github.com/klauspost/compress v1.18.0 10 | github.com/quic-go/qpack v0.5.1 11 | github.com/quic-go/quic-go v0.51.0 12 | github.com/refraction-networking/utls v1.6.7 13 | golang.org/x/net v0.39.0 14 | golang.org/x/text v0.24.0 15 | ) 16 | 17 | require ( 18 | github.com/cloudflare/circl v1.6.1 // indirect 19 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 20 | github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect 21 | github.com/hashicorp/errwrap v1.1.0 // indirect 22 | github.com/onsi/ginkgo/v2 v2.23.4 // indirect 23 | go.uber.org/automaxprocs v1.6.0 // indirect 24 | go.uber.org/mock v0.5.1 // indirect 25 | golang.org/x/crypto v0.37.0 // indirect 26 | golang.org/x/mod v0.24.0 // indirect 27 | golang.org/x/sync v0.13.0 // indirect 28 | golang.org/x/sys v0.32.0 // indirect 29 | golang.org/x/tools v0.32.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 4 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 8 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 9 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 10 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 11 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 12 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 13 | github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4= 14 | github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= 15 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 16 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 17 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 18 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 19 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 20 | github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= 21 | github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= 22 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 23 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 24 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 25 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 26 | github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= 27 | github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 31 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 32 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 33 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 34 | github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc= 35 | github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= 36 | github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= 37 | github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= 38 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 39 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 40 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 41 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 42 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 43 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 44 | go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= 45 | go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 46 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 47 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 48 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 49 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 50 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 51 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 52 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 53 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 54 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 55 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 56 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 57 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 58 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 59 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 60 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 61 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 62 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 63 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 65 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 66 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/textproto" 7 | "sort" 8 | "strings" 9 | "sync" 10 | 11 | "golang.org/x/net/http/httpguts" 12 | 13 | "github.com/imroc/req/v3/internal/header" 14 | ) 15 | 16 | var headerNewlineToSpace = strings.NewReplacer("\n", " ", "\r", " ") 17 | 18 | // stringWriter implements WriteString on a Writer. 19 | type stringWriter struct { 20 | w io.Writer 21 | } 22 | 23 | func (w stringWriter) WriteString(s string) (n int, err error) { 24 | return w.w.Write([]byte(s)) 25 | } 26 | 27 | // A headerSorter implements sort.Interface by sorting a []keyValues 28 | // by key. It's used as a pointer, so it can fit in a sort.Interface 29 | // interface value without allocation. 30 | type headerSorter struct { 31 | kvs []header.KeyValues 32 | } 33 | 34 | func (s *headerSorter) Len() int { return len(s.kvs) } 35 | func (s *headerSorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] } 36 | func (s *headerSorter) Less(i, j int) bool { return s.kvs[i].Key < s.kvs[j].Key } 37 | 38 | var headerSorterPool = sync.Pool{ 39 | New: func() interface{} { return new(headerSorter) }, 40 | } 41 | 42 | // get is like Get, but key must already be in CanonicalHeaderKey form. 43 | func headerGet(h http.Header, key string) string { 44 | if v := h[key]; len(v) > 0 { 45 | return v[0] 46 | } 47 | return "" 48 | } 49 | 50 | // has reports whether h has the provided key defined, even if it's 51 | // set to 0-length slice. 52 | func headerHas(h http.Header, key string) bool { 53 | _, ok := h[key] 54 | return ok 55 | } 56 | 57 | // sortedKeyValues returns h's keys sorted in the returned kvs 58 | // slice. The headerSorter used to sort is also returned, for possible 59 | // return to headerSorterCache. 60 | func headerSortedKeyValues(h http.Header, exclude map[string]bool) (kvs []header.KeyValues, hs *headerSorter) { 61 | hs = headerSorterPool.Get().(*headerSorter) 62 | if cap(hs.kvs) < len(h) { 63 | hs.kvs = make([]header.KeyValues, 0, len(h)) 64 | } 65 | kvs = hs.kvs[:0] 66 | for k, vv := range h { 67 | if !exclude[k] { 68 | kvs = append(kvs, header.KeyValues{k, vv}) 69 | } 70 | } 71 | hs.kvs = kvs 72 | sort.Sort(hs) 73 | return kvs, hs 74 | } 75 | 76 | func headerWrite(h http.Header, writeHeader func(key string, values ...string) error, sort bool) error { 77 | return headerWriteSubset(h, nil, writeHeader, sort) 78 | } 79 | 80 | func headerWriteSubset(h http.Header, exclude map[string]bool, writeHeader func(key string, values ...string) error, sort bool) error { 81 | var kvs []header.KeyValues 82 | var hs *headerSorter 83 | if sort { 84 | kvs = make([]header.KeyValues, 0, len(h)) 85 | for k, v := range h { 86 | if !exclude[k] { 87 | kvs = append(kvs, header.KeyValues{k, v}) 88 | } 89 | } 90 | } else { 91 | kvs, hs = headerSortedKeyValues(h, exclude) 92 | } 93 | for _, kv := range kvs { 94 | if !httpguts.ValidHeaderFieldName(kv.Key) { 95 | // This could be an error. In the common case of 96 | // writing response headers, however, we have no good 97 | // way to provide the error back to the server 98 | // handler, so just drop invalid headers instead. 99 | continue 100 | } 101 | for i, v := range kv.Values { 102 | vv := headerNewlineToSpace.Replace(v) 103 | vv = textproto.TrimString(vv) 104 | if vv != v { 105 | kv.Values[i] = vv 106 | } 107 | } 108 | err := writeHeader(kv.Key, kv.Values...) 109 | if err != nil { 110 | if hs != nil { 111 | headerSorterPool.Put(hs) 112 | } 113 | return err 114 | } 115 | } 116 | if hs != nil { 117 | headerSorterPool.Put(hs) 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /http2/priority.go: -------------------------------------------------------------------------------- 1 | package http2 2 | 3 | // PriorityParam are the stream prioritzation parameters. 4 | type PriorityParam struct { 5 | // StreamDep is a 31-bit stream identifier for the 6 | // stream that this stream depends on. Zero means no 7 | // dependency. 8 | StreamDep uint32 9 | 10 | // Exclusive is whether the dependency is exclusive. 11 | Exclusive bool 12 | 13 | // Weight is the stream's zero-indexed weight. It should be 14 | // set together with StreamDep, or neither should be set. Per 15 | // the spec, "Add one to the value to obtain a weight between 16 | // 1 and 256." 17 | Weight uint8 18 | } 19 | 20 | func (p PriorityParam) IsZero() bool { 21 | return p == PriorityParam{} 22 | } 23 | 24 | // PriorityFrame represents a http priority frame. 25 | type PriorityFrame struct { 26 | StreamID uint32 27 | PriorityParam PriorityParam 28 | } 29 | -------------------------------------------------------------------------------- /http2/setting.go: -------------------------------------------------------------------------------- 1 | package http2 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // A SettingID is an HTTP/2 setting as defined in 8 | // https://httpwg.org/specs/rfc7540.html#iana-settings 9 | type SettingID uint16 10 | 11 | const ( 12 | SettingHeaderTableSize SettingID = 0x1 13 | SettingEnablePush SettingID = 0x2 14 | SettingMaxConcurrentStreams SettingID = 0x3 15 | SettingInitialWindowSize SettingID = 0x4 16 | SettingMaxFrameSize SettingID = 0x5 17 | SettingMaxHeaderListSize SettingID = 0x6 18 | ) 19 | 20 | var settingName = map[SettingID]string{ 21 | SettingHeaderTableSize: "HEADER_TABLE_SIZE", 22 | SettingEnablePush: "ENABLE_PUSH", 23 | SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS", 24 | SettingInitialWindowSize: "INITIAL_WINDOW_SIZE", 25 | SettingMaxFrameSize: "MAX_FRAME_SIZE", 26 | SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", 27 | } 28 | 29 | func (s SettingID) String() string { 30 | if v, ok := settingName[s]; ok { 31 | return v 32 | } 33 | return fmt.Sprintf("UNKNOWN_SETTING_%d", uint16(s)) 34 | } 35 | 36 | // Setting is a setting parameter: which setting it is, and its value. 37 | type Setting struct { 38 | // ID is which setting is being set. 39 | // See https://httpwg.org/specs/rfc7540.html#SettingValues 40 | ID SettingID 41 | 42 | // Val is the value. 43 | Val uint32 44 | } 45 | 46 | func (s Setting) String() string { 47 | return fmt.Sprintf("[%v = %d]", s.ID, s.Val) 48 | } 49 | -------------------------------------------------------------------------------- /http_request.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | "golang.org/x/net/http/httpguts" 9 | 10 | "github.com/imroc/req/v3/internal/ascii" 11 | "github.com/imroc/req/v3/internal/header" 12 | ) 13 | 14 | // Given a string of the form "host", "host:port", or "[ipv6::address]:port", 15 | // return true if the string includes a port. 16 | func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } 17 | 18 | // removeEmptyPort strips the empty port in ":port" to "" 19 | // as mandated by RFC 3986 Section 6.2.3. 20 | func removeEmptyPort(host string) string { 21 | if hasPort(host) { 22 | return strings.TrimSuffix(host, ":") 23 | } 24 | return host 25 | } 26 | 27 | func isNotToken(r rune) bool { 28 | return !httpguts.IsTokenRune(r) 29 | } 30 | 31 | func validMethod(method string) bool { 32 | /* 33 | Method = "OPTIONS" ; Section 9.2 34 | | "GET" ; Section 9.3 35 | | "HEAD" ; Section 9.4 36 | | "POST" ; Section 9.5 37 | | "PUT" ; Section 9.6 38 | | "DELETE" ; Section 9.7 39 | | "TRACE" ; Section 9.8 40 | | "CONNECT" ; Section 9.9 41 | | extension-method 42 | extension-method = token 43 | token = 1* 44 | */ 45 | return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1 46 | } 47 | 48 | func closeBody(r *http.Request) error { 49 | if r.Body == nil { 50 | return nil 51 | } 52 | return r.Body.Close() 53 | } 54 | 55 | // requestBodyReadError wraps an error from (*Request).write to indicate 56 | // that the error came from a Read call on the Request.Body. 57 | // This error type should not escape the net/http package to users. 58 | type requestBodyReadError struct{ error } 59 | 60 | // Return value if nonempty, def otherwise. 61 | func valueOrDefault(value, def string) string { 62 | if value != "" { 63 | return value 64 | } 65 | return def 66 | } 67 | 68 | // outgoingLength reports the Content-Length of this outgoing (Client) request. 69 | // It maps 0 into -1 (unknown) when the Body is non-nil. 70 | func outgoingLength(r *http.Request) int64 { 71 | if r.Body == nil || r.Body == NoBody { 72 | return 0 73 | } 74 | if r.ContentLength != 0 { 75 | return r.ContentLength 76 | } 77 | return -1 78 | } 79 | 80 | // errMissingHost is returned by Write when there is no Host or URL present in 81 | // the Request. 82 | var errMissingHost = errors.New("http: Request.Write on Request with no Host or URL set") 83 | 84 | func closeRequestBody(r *http.Request) error { 85 | if r.Body == nil { 86 | return nil 87 | } 88 | return r.Body.Close() 89 | } 90 | 91 | // Headers that Request.Write handles itself and should be skipped. 92 | var reqWriteExcludeHeader = map[string]bool{ 93 | "Host": true, // not in Header map anyway 94 | "User-Agent": true, 95 | "Content-Length": true, 96 | "Transfer-Encoding": true, 97 | "Trailer": true, 98 | header.HeaderOderKey: true, 99 | header.PseudoHeaderOderKey: true, 100 | } 101 | 102 | // requestMethodUsuallyLacksBody reports whether the given request 103 | // method is one that typically does not involve a request body. 104 | // This is used by the Transport (via 105 | // transferWriter.shouldSendChunkedRequestBody) to determine whether 106 | // we try to test-read a byte from a non-nil Request.Body when 107 | // Request.outgoingLength() returns -1. See the comments in 108 | // shouldSendChunkedRequestBody. 109 | func requestMethodUsuallyLacksBody(method string) bool { 110 | switch method { 111 | case "GET", "HEAD", "DELETE", "OPTIONS", "PROPFIND", "SEARCH": 112 | return true 113 | } 114 | return false 115 | } 116 | 117 | // requiresHTTP1 reports whether this request requires being sent on 118 | // an HTTP/1 connection. 119 | func requestRequiresHTTP1(r *http.Request) bool { 120 | return hasToken(r.Header.Get("Connection"), "upgrade") && 121 | ascii.EqualFold(r.Header.Get("Upgrade"), "websocket") 122 | } 123 | 124 | func isReplayable(r *http.Request) bool { 125 | if r.Body == nil || r.Body == NoBody || r.GetBody != nil { 126 | switch valueOrDefault(r.Method, "GET") { 127 | case "GET", "HEAD", "OPTIONS", "TRACE": 128 | return true 129 | } 130 | // The Idempotency-Key, while non-standard, is widely used to 131 | // mean a POST or other request is idempotent. See 132 | // https://golang.org/issue/19943#issuecomment-421092421 133 | if headerHas(r.Header, "Idempotency-Key") || headerHas(r.Header, "X-Idempotency-Key") { 134 | return true 135 | } 136 | } 137 | return false 138 | } 139 | 140 | func reqExpectsContinue(r *http.Request) bool { 141 | return hasToken(headerGet(r.Header, "Expect"), "100-continue") 142 | } 143 | 144 | func reqWantsClose(r *http.Request) bool { 145 | if r.Close { 146 | return true 147 | } 148 | return hasToken(headerGet(r.Header, "Connection"), "close") 149 | } 150 | -------------------------------------------------------------------------------- /internal/altsvcutil/altsvcutil.go: -------------------------------------------------------------------------------- 1 | package altsvcutil 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/imroc/req/v3/internal/netutil" 7 | "github.com/imroc/req/v3/pkg/altsvc" 8 | "io" 9 | "net" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type altAvcParser struct { 17 | *bytes.Buffer 18 | } 19 | 20 | // validOptionalPort reports whether port is either an empty string 21 | // or matches /^:\d*$/ 22 | func validOptionalPort(port string) bool { 23 | if port == "" { 24 | return true 25 | } 26 | if port[0] != ':' { 27 | return false 28 | } 29 | for _, b := range port[1:] { 30 | if b < '0' || b > '9' { 31 | return false 32 | } 33 | } 34 | return true 35 | } 36 | 37 | // splitHostPort separates host and port. If the port is not valid, it returns 38 | // the entire input as host, and it doesn't check the validity of the host. 39 | // Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. 40 | func splitHostPort(hostPort string) (host, port string) { 41 | host = hostPort 42 | 43 | colon := strings.LastIndexByte(host, ':') 44 | if colon != -1 && validOptionalPort(host[colon:]) { 45 | host, port = host[:colon], host[colon+1:] 46 | } 47 | 48 | if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { 49 | host = host[1 : len(host)-1] 50 | } 51 | return 52 | } 53 | 54 | // ParseHeader parses the AltSvc from header value. 55 | func ParseHeader(value string) ([]*altsvc.AltSvc, error) { 56 | p := newAltSvcParser(value) 57 | return p.Parse() 58 | } 59 | 60 | func newAltSvcParser(value string) *altAvcParser { 61 | buf := bytes.NewBufferString(value) 62 | return &altAvcParser{buf} 63 | } 64 | 65 | var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) 66 | 67 | func (p *altAvcParser) Parse() (as []*altsvc.AltSvc, err error) { 68 | for { 69 | a, e := p.parseOne() 70 | if a != nil { 71 | as = append(as, a) 72 | } 73 | if e != nil { 74 | if e == io.EOF { 75 | return 76 | } else { 77 | err = e 78 | return 79 | } 80 | } 81 | } 82 | } 83 | 84 | func (p *altAvcParser) parseKv() (key, value string, haveNextField bool, err error) { 85 | line, err := p.ReadBytes('=') 86 | if len(line) == 0 { 87 | return 88 | } 89 | key = strings.TrimSpace(string(line[:len(line)-1])) 90 | bs := p.Bytes() 91 | if len(bs) == 0 { 92 | err = io.EOF 93 | return 94 | } 95 | if bs[0] == '"' { 96 | quoteIndex := 0 97 | for i := 1; i < len(bs); i++ { 98 | if bs[i] == '"' { 99 | quoteIndex = i 100 | break 101 | } 102 | } 103 | if quoteIndex == 0 { 104 | err = fmt.Errorf("quote in alt-svc is not complete: %s", bs) 105 | return 106 | } 107 | value = string(bs[1:quoteIndex]) 108 | p.Next(quoteIndex + 1) 109 | if len(bs) == quoteIndex+1 { 110 | err = io.EOF 111 | return 112 | } 113 | var b byte 114 | b, err = p.ReadByte() 115 | if err != nil { 116 | return 117 | } 118 | if b == ';' { 119 | haveNextField = true 120 | } 121 | } else { 122 | delimIndex := 0 123 | LOOP: 124 | for i, v := range bs { 125 | switch v { 126 | case ',': 127 | delimIndex = i 128 | break LOOP 129 | case ';': 130 | delimIndex = i 131 | haveNextField = true 132 | break LOOP 133 | } 134 | } 135 | if delimIndex == 0 { 136 | err = io.EOF 137 | value = strings.TrimSpace(string(bs)) 138 | return 139 | } 140 | p.Next(delimIndex + 1) 141 | value = string(bs[:delimIndex]) 142 | } 143 | return 144 | } 145 | 146 | func (p *altAvcParser) parseOne() (as *altsvc.AltSvc, err error) { 147 | proto, addr, haveNextField, err := p.parseKv() 148 | if proto == "" || addr == "" { 149 | return 150 | } 151 | host, port := splitHostPort(addr) 152 | 153 | as = &altsvc.AltSvc{ 154 | Protocol: proto, 155 | Host: host, 156 | Port: port, 157 | Expire: endOfTime, 158 | } 159 | 160 | if !haveNextField { 161 | return 162 | } 163 | 164 | key, ma, haveNextField, err := p.parseKv() 165 | if key == "" || ma == "" { 166 | return 167 | } 168 | if key != "ma" { 169 | err = fmt.Errorf("expect ma field, got %s", key) 170 | return 171 | } 172 | 173 | maInt, err := strconv.ParseInt(ma, 10, 64) 174 | if err != nil { 175 | return 176 | } 177 | as.Expire = time.Now().Add(time.Duration(maInt) * time.Second) 178 | 179 | if !haveNextField { 180 | return 181 | } 182 | 183 | // drain useless fields 184 | for { 185 | _, _, haveNextField, err = p.parseKv() 186 | if haveNextField { 187 | continue 188 | } else { 189 | break 190 | } 191 | } 192 | return 193 | } 194 | 195 | // ConvertURL converts the raw request url to expected alt-svc's url. 196 | func ConvertURL(a *altsvc.AltSvc, u *url.URL) *url.URL { 197 | host, port := netutil.AuthorityHostPort(u.Scheme, u.Host) 198 | uu := *u 199 | modify := false 200 | if a.Host != "" && a.Host != host { 201 | host = a.Host 202 | modify = true 203 | } 204 | if a.Port != "" && a.Port != port { 205 | port = a.Port 206 | modify = true 207 | } 208 | if modify { 209 | uu.Host = net.JoinHostPort(host, port) 210 | } 211 | return &uu 212 | } 213 | -------------------------------------------------------------------------------- /internal/altsvcutil/altsvcutil_test.go: -------------------------------------------------------------------------------- 1 | package altsvcutil 2 | 3 | import ( 4 | "github.com/imroc/req/v3/internal/tests" 5 | "testing" 6 | ) 7 | 8 | func TestParseHeader(t *testing.T) { 9 | as, err := ParseHeader(` h3=":443"; ma=86400, h3-29=":443"; ma=86400`) 10 | tests.AssertNoError(t, err) 11 | tests.AssertEqual(t, 2, len(as)) 12 | tests.AssertEqual(t, "h3", as[0].Protocol) 13 | tests.AssertEqual(t, "443", as[0].Port) 14 | } 15 | -------------------------------------------------------------------------------- /internal/ascii/print.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ascii 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | // EqualFold is strings.EqualFold, ASCII only. It reports whether s and t 13 | // are equal, ASCII-case-insensitively. 14 | func EqualFold(s, t string) bool { 15 | if len(s) != len(t) { 16 | return false 17 | } 18 | for i := 0; i < len(s); i++ { 19 | if lower(s[i]) != lower(t[i]) { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | // lower returns the ASCII lowercase version of b. 27 | func lower(b byte) byte { 28 | if 'A' <= b && b <= 'Z' { 29 | return b + ('a' - 'A') 30 | } 31 | return b 32 | } 33 | 34 | // IsPrint returns whether s is ASCII and printable according to 35 | // https://tools.ietf.org/html/rfc20#section-4.2. 36 | func IsPrint(s string) bool { 37 | for i := 0; i < len(s); i++ { 38 | if s[i] < ' ' || s[i] > '~' { 39 | return false 40 | } 41 | } 42 | return true 43 | } 44 | 45 | // Is returns whether s is ASCII. 46 | func Is(s string) bool { 47 | for i := 0; i < len(s); i++ { 48 | if s[i] > unicode.MaxASCII { 49 | return false 50 | } 51 | } 52 | return true 53 | } 54 | 55 | // ToLower returns the lowercase version of s if s is ASCII and printable. 56 | func ToLower(s string) (lower string, ok bool) { 57 | if !IsPrint(s) { 58 | return "", false 59 | } 60 | return strings.ToLower(s), true 61 | } 62 | -------------------------------------------------------------------------------- /internal/ascii/print_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ascii 6 | 7 | import "testing" 8 | 9 | func TestEqualFold(t *testing.T) { 10 | var tests = []struct { 11 | name string 12 | a, b string 13 | want bool 14 | }{ 15 | { 16 | name: "empty", 17 | want: true, 18 | }, 19 | { 20 | name: "simple match", 21 | a: "CHUNKED", 22 | b: "chunked", 23 | want: true, 24 | }, 25 | { 26 | name: "same string", 27 | a: "chunked", 28 | b: "chunked", 29 | want: true, 30 | }, 31 | { 32 | name: "Unicode Kelvin symbol", 33 | a: "chunKed", // This "K" is 'KELVIN SIGN' (\u212A) 34 | b: "chunked", 35 | want: false, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | if got := EqualFold(tt.a, tt.b); got != tt.want { 41 | t.Errorf("AsciiEqualFold(%q,%q): got %v want %v", tt.a, tt.b, got, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestIsPrint(t *testing.T) { 48 | var tests = []struct { 49 | name string 50 | in string 51 | want bool 52 | }{ 53 | { 54 | name: "empty", 55 | want: true, 56 | }, 57 | { 58 | name: "ASCII low", 59 | in: "This is a space: ' '", 60 | want: true, 61 | }, 62 | { 63 | name: "ASCII high", 64 | in: "This is a tilde: '~'", 65 | want: true, 66 | }, 67 | { 68 | name: "ASCII low non-print", 69 | in: "This is a unit separator: \x1F", 70 | want: false, 71 | }, 72 | { 73 | name: "Ascii high non-print", 74 | in: "This is a Delete: \x7F", 75 | want: false, 76 | }, 77 | { 78 | name: "Unicode letter", 79 | in: "Today it's 280K outside: it's freezing!", // This "K" is 'KELVIN SIGN' (\u212A) 80 | want: false, 81 | }, 82 | { 83 | name: "Unicode emoji", 84 | in: "Gophers like 🧀", 85 | want: false, 86 | }, 87 | } 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | if got := IsPrint(tt.in); got != tt.want { 91 | t.Errorf("IsASCIIPrint(%q): got %v want %v", tt.in, got, tt.want) 92 | } 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/charsets/.testdata/HTTP-vs-UTF-8-BOM.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | HTTP vs UTF-8 BOM 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 |

HTTP vs UTF-8 BOM

18 | 19 | 20 |
21 | 22 | 23 |
 
24 | 25 | 26 | 27 | 28 | 29 |
30 |

A character encoding set in the HTTP header has lower precedence than the UTF-8 signature.

31 |

The HTTP header attempts to set the character encoding to ISO 8859-15. The page starts with a UTF-8 signature.

The test contains a div with a class name that contains the following sequence of bytes: 0xC3 0xBD 0xC3 0xA4 0xC3 0xA8. These represent different sequences of characters in ISO 8859-15, ISO 8859-1 and UTF-8. The external, UTF-8-encoded stylesheet contains a selector .test div.ýäè. This matches the sequence of bytes above when they are interpreted as UTF-8. If the class name matches the selector then the test will pass.

If the test is unsuccessful, the characters  should appear at the top of the page. These represent the bytes that make up the UTF-8 signature when encountered in the ISO 8859-15 encoding.

32 |
33 |
34 |
Next test
HTML5
35 |

the-input-byte-stream-034
Result summary & related tests
Detailed results for this test
Link to spec

36 |
Assumptions:
  • The default encoding for the browser you are testing is not set to ISO 8859-15.
  • 37 |
  • The test is read from a server that supports HTTP.
38 |
39 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /internal/charsets/.testdata/UTF-16BE-BOM.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/req/8ef060891595f8f804a62a5b8d12a60beaa00012/internal/charsets/.testdata/UTF-16BE-BOM.html -------------------------------------------------------------------------------- /internal/charsets/.testdata/UTF-16LE-BOM.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/req/8ef060891595f8f804a62a5b8d12a60beaa00012/internal/charsets/.testdata/UTF-16LE-BOM.html -------------------------------------------------------------------------------- /internal/charsets/.testdata/UTF-8-BOM-vs-meta-charset.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | UTF-8 BOM vs meta charset 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 |

UTF-8 BOM vs meta charset

19 | 20 | 21 |
22 | 23 | 24 |
 
25 | 26 | 27 | 28 | 29 | 30 |
31 |

A page with a UTF-8 BOM will be recognized as UTF-8 even if the meta charset attribute declares a different encoding.

32 |

The page contains an encoding declaration in a meta charset attribute that attempts to set the character encoding to ISO 8859-15, but the file starts with a UTF-8 signature.

The test contains a div with a class name that contains the following sequence of bytes: 0xC3 0xBD 0xC3 0xA4 0xC3 0xA8. These represent different sequences of characters in ISO 8859-15, ISO 8859-1 and UTF-8. The external, UTF-8-encoded stylesheet contains a selector .test div.ýäè. This matches the sequence of bytes above when they are interpreted as UTF-8. If the class name matches the selector then the test will pass.

33 |
34 |
35 |
Next test
HTML5
36 |

the-input-byte-stream-038
Result summary & related tests
Detailed results for this test
Link to spec

37 |
Assumptions:
  • The default encoding for the browser you are testing is not set to ISO 8859-15.
  • 38 |
  • The test is read from a server that supports HTTP.
39 |
40 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /internal/charsets/.testdata/UTF-8-BOM-vs-meta-content.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | UTF-8 BOM vs meta content 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 |

UTF-8 BOM vs meta content

18 | 19 | 20 |
21 | 22 | 23 |
 
24 | 25 | 26 | 27 | 28 | 29 |
30 |

A page with a UTF-8 BOM will be recognized as UTF-8 even if the meta content attribute declares a different encoding.

31 |

The page contains an encoding declaration in a meta content attribute that attempts to set the character encoding to ISO 8859-15, but the file starts with a UTF-8 signature.

The test contains a div with a class name that contains the following sequence of bytes: 0xC3 0xBD 0xC3 0xA4 0xC3 0xA8. These represent different sequences of characters in ISO 8859-15, ISO 8859-1 and UTF-8. The external, UTF-8-encoded stylesheet contains a selector .test div.ýäè. This matches the sequence of bytes above when they are interpreted as UTF-8. If the class name matches the selector then the test will pass.

32 |
33 |
34 |
Next test
HTML5
35 |

the-input-byte-stream-037
Result summary & related tests
Detailed results for this test
Link to spec

36 |
Assumptions:
  • The default encoding for the browser you are testing is not set to ISO 8859-15.
  • 37 |
  • The test is read from a server that supports HTTP.
38 |
39 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /internal/charsets/.testdata/meta-charset-attribute.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | meta charset attribute 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 |

meta charset attribute

18 | 19 | 20 |
21 | 22 | 23 |
 
24 | 25 | 26 | 27 | 28 | 29 |
30 |

The character encoding of the page can be set by a meta element with charset attribute.

31 |

The only character encoding declaration for this HTML file is in the charset attribute of the meta element, which declares the encoding to be ISO 8859-15.

The test contains a div with a class name that contains the following sequence of bytes: 0xC3 0xBD 0xC3 0xA4 0xC3 0xA8. These represent different sequences of characters in ISO 8859-15, ISO 8859-1 and UTF-8. The external, UTF-8-encoded stylesheet contains a selector .test div.ÜÀÚ. This matches the sequence of bytes above when they are interpreted as ISO 8859-15. If the class name matches the selector then the test will pass.

32 |
33 |
34 |
Next test
HTML5
35 |

the-input-byte-stream-009
Result summary & related tests
Detailed results for this test
Link to spec

36 |
Assumptions:
  • The default encoding for the browser you are testing is not set to ISO 8859-15.
  • 37 |
  • The test is read from a server that supports HTTP.
38 |
39 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /internal/charsets/.testdata/meta-content-attribute.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | meta content attribute 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 |

meta content attribute

18 | 19 | 20 |
21 | 22 | 23 |
 
24 | 25 | 26 | 27 | 28 | 29 |
30 |

The character encoding of the page can be set by a meta element with http-equiv and content attributes.

31 |

The only character encoding declaration for this HTML file is in the content attribute of the meta element, which declares the encoding to be ISO 8859-15.

The test contains a div with a class name that contains the following sequence of bytes: 0xC3 0xBD 0xC3 0xA4 0xC3 0xA8. These represent different sequences of characters in ISO 8859-15, ISO 8859-1 and UTF-8. The external, UTF-8-encoded stylesheet contains a selector .test div.ÜÀÚ. This matches the sequence of bytes above when they are interpreted as ISO 8859-15. If the class name matches the selector then the test will pass.

32 |
33 |
34 |
Next test
HTML5
35 |

the-input-byte-stream-007
Result summary & related tests
Detailed results for this test
Link to spec

36 |
Assumptions:
  • The default encoding for the browser you are testing is not set to ISO 8859-15.
  • 37 |
  • The test is read from a server that supports HTTP.
38 |
39 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /internal/charsets/charsets.go: -------------------------------------------------------------------------------- 1 | package charsets 2 | 3 | import ( 4 | "bytes" 5 | "golang.org/x/net/html" 6 | htmlcharset "golang.org/x/net/html/charset" 7 | "golang.org/x/text/encoding" 8 | "strings" 9 | ) 10 | 11 | var boms = []struct { 12 | bom []byte 13 | enc string 14 | }{ 15 | {[]byte{0xfe, 0xff}, "utf-16be"}, 16 | {[]byte{0xff, 0xfe}, "utf-16le"}, 17 | {[]byte{0xef, 0xbb, 0xbf}, "utf-8"}, 18 | } 19 | 20 | // FindEncoding sniff and find the encoding of the content. 21 | func FindEncoding(content []byte) (enc encoding.Encoding, name string) { 22 | if len(content) == 0 { 23 | return 24 | } 25 | for _, b := range boms { 26 | if bytes.HasPrefix(content, b.bom) { 27 | enc, name = htmlcharset.Lookup(b.enc) 28 | if enc != nil { 29 | if strings.ToLower(name) == "utf-8" { 30 | enc = nil 31 | } 32 | return 33 | } 34 | } 35 | } 36 | enc, name = prescan(content) 37 | if strings.ToLower(name) == "utf-8" { 38 | enc = nil 39 | } 40 | return 41 | } 42 | 43 | func prescan(content []byte) (e encoding.Encoding, name string) { 44 | z := html.NewTokenizer(bytes.NewReader(content)) 45 | for { 46 | switch z.Next() { 47 | case html.ErrorToken: 48 | return nil, "" 49 | 50 | case html.StartTagToken, html.SelfClosingTagToken: 51 | tagName, hasAttr := z.TagName() 52 | if !bytes.Equal(tagName, []byte("meta")) { 53 | continue 54 | } 55 | attrList := make(map[string]bool) 56 | gotPragma := false 57 | 58 | const ( 59 | dontKnow = iota 60 | doNeedPragma 61 | doNotNeedPragma 62 | ) 63 | needPragma := dontKnow 64 | 65 | name = "" 66 | e = nil 67 | for hasAttr { 68 | var key, val []byte 69 | key, val, hasAttr = z.TagAttr() 70 | ks := string(key) 71 | if attrList[ks] { 72 | continue 73 | } 74 | attrList[ks] = true 75 | for i, c := range val { 76 | if 'A' <= c && c <= 'Z' { 77 | val[i] = c + 0x20 78 | } 79 | } 80 | 81 | switch ks { 82 | case "http-equiv": 83 | if bytes.Equal(val, []byte("content-type")) { 84 | gotPragma = true 85 | } 86 | 87 | case "content": 88 | if e == nil { 89 | name = fromMetaElement(string(val)) 90 | if name != "" { 91 | e, name = htmlcharset.Lookup(name) 92 | if e != nil { 93 | needPragma = doNeedPragma 94 | } 95 | } 96 | } 97 | 98 | case "charset": 99 | e, name = htmlcharset.Lookup(string(val)) 100 | needPragma = doNotNeedPragma 101 | } 102 | } 103 | 104 | if needPragma == dontKnow || needPragma == doNeedPragma && !gotPragma { 105 | continue 106 | } 107 | 108 | if strings.HasPrefix(name, "utf-16") { 109 | name = "utf-8" 110 | e = encoding.Nop 111 | } 112 | 113 | if e != nil { 114 | return e, name 115 | } 116 | } 117 | } 118 | } 119 | 120 | func fromMetaElement(s string) string { 121 | for s != "" { 122 | csLoc := strings.Index(s, "charset") 123 | if csLoc == -1 { 124 | return "" 125 | } 126 | s = s[csLoc+len("charset"):] 127 | s = strings.TrimLeft(s, " \t\n\f\r") 128 | if !strings.HasPrefix(s, "=") { 129 | continue 130 | } 131 | s = s[1:] 132 | s = strings.TrimLeft(s, " \t\n\f\r") 133 | if s == "" { 134 | return "" 135 | } 136 | if q := s[0]; q == '"' || q == '\'' { 137 | s = s[1:] 138 | closeQuote := strings.IndexRune(s, rune(q)) 139 | if closeQuote == -1 { 140 | return "" 141 | } 142 | return s[:closeQuote] 143 | } 144 | 145 | end := strings.IndexAny(s, "; \t\n\f\r") 146 | if end == -1 { 147 | end = len(s) 148 | } 149 | return s[:end] 150 | } 151 | return "" 152 | } 153 | -------------------------------------------------------------------------------- /internal/charsets/charsets_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package charsets 6 | 7 | import ( 8 | "github.com/imroc/req/v3/internal/tests" 9 | "os" 10 | "runtime" 11 | "testing" 12 | ) 13 | 14 | var sniffTestCases = []struct { 15 | filename, want string 16 | }{ 17 | {"UTF-16LE-BOM.html", "utf-16le"}, 18 | {"UTF-16BE-BOM.html", "utf-16be"}, 19 | {"meta-content-attribute.html", "iso-8859-15"}, 20 | {"meta-charset-attribute.html", "iso-8859-15"}, 21 | {"HTTP-vs-UTF-8-BOM.html", "utf-8"}, 22 | {"UTF-8-BOM-vs-meta-content.html", "utf-8"}, 23 | {"UTF-8-BOM-vs-meta-charset.html", "utf-8"}, 24 | } 25 | 26 | func TestSniff(t *testing.T) { 27 | switch runtime.GOOS { 28 | case "nacl": // platforms that don't permit direct file system access 29 | t.Skipf("not supported on %q", runtime.GOOS) 30 | } 31 | 32 | for _, tc := range sniffTestCases { 33 | content, err := os.ReadFile(tests.GetTestFilePath(tc.filename)) 34 | if err != nil { 35 | t.Errorf("%s: error reading file: %v", tc.filename, err) 36 | continue 37 | } 38 | 39 | _, name := FindEncoding(content) 40 | if name != tc.want { 41 | t.Errorf("%s: got %q, want %q", tc.filename, name, tc.want) 42 | continue 43 | } 44 | } 45 | } 46 | 47 | var metaTestCases = []struct { 48 | meta, want string 49 | }{ 50 | {"", ""}, 51 | {"text/html", ""}, 52 | {"text/html; charset utf-8", ""}, 53 | {"text/html; charset=latin-2", "latin-2"}, 54 | {"text/html; charset; charset = utf-8", "utf-8"}, 55 | {`charset="big5"`, "big5"}, 56 | {"charset='shift_jis'", "shift_jis"}, 57 | } 58 | 59 | func TestFromMeta(t *testing.T) { 60 | for _, tc := range metaTestCases { 61 | got := fromMetaElement(tc.meta) 62 | if got != tc.want { 63 | t.Errorf("%q: got %q, want %q", tc.meta, got, tc.want) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/common/error.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | // ErrRequestCanceled is a copy of net/http's common.ErrRequestCanceled because it's not 6 | // exported. At least they'll be DeepEqual for h1-vs-h2 comparisons tests. 7 | var ErrRequestCanceled = errors.New("net/http: request canceled") 8 | -------------------------------------------------------------------------------- /internal/compress/brotli_reader.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/andybalholm/brotli" 7 | ) 8 | 9 | type BrotliReader struct { 10 | Body io.ReadCloser // underlying Response.Body 11 | br io.Reader // lazily-initialized brotli reader 12 | berr error // sticky error 13 | } 14 | 15 | func NewBrotliReader(body io.ReadCloser) *BrotliReader { 16 | return &BrotliReader{Body: body} 17 | } 18 | 19 | func (br *BrotliReader) Read(p []byte) (n int, err error) { 20 | if br.berr != nil { 21 | return 0, br.berr 22 | } 23 | if br.br == nil { 24 | br.br = brotli.NewReader(br.Body) 25 | } 26 | return br.br.Read(p) 27 | } 28 | 29 | func (br *BrotliReader) Close() error { 30 | return br.Body.Close() 31 | } 32 | 33 | func (br *BrotliReader) GetUnderlyingBody() io.ReadCloser { 34 | return br.Body 35 | } 36 | 37 | func (br *BrotliReader) SetUnderlyingBody(body io.ReadCloser) { 38 | br.Body = body 39 | } 40 | -------------------------------------------------------------------------------- /internal/compress/deflate_reader.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "compress/flate" 5 | "io" 6 | ) 7 | 8 | type DeflateReader struct { 9 | Body io.ReadCloser // underlying Response.Body 10 | dr io.ReadCloser // lazily-initialized deflate reader 11 | derr error // sticky error 12 | } 13 | 14 | func NewDeflateReader(body io.ReadCloser) *DeflateReader { 15 | return &DeflateReader{Body: body} 16 | } 17 | 18 | func (df *DeflateReader) Read(p []byte) (n int, err error) { 19 | if df.derr != nil { 20 | return 0, df.derr 21 | } 22 | if df.dr == nil { 23 | df.dr = flate.NewReader(df.Body) 24 | } 25 | return df.dr.Read(p) 26 | } 27 | 28 | func (df *DeflateReader) Close() error { 29 | if df.dr != nil { 30 | return df.dr.Close() 31 | } 32 | return df.Body.Close() 33 | } 34 | 35 | func (df *DeflateReader) GetUnderlyingBody() io.ReadCloser { 36 | return df.Body 37 | } 38 | 39 | func (df *DeflateReader) SetUnderlyingBody(body io.ReadCloser) { 40 | df.Body = body 41 | } 42 | -------------------------------------------------------------------------------- /internal/compress/gzip_reader.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | "io/fs" 7 | ) 8 | 9 | // GzipReader wraps a response body so it can lazily 10 | // call gzip.NewReader on the first call to Read 11 | type GzipReader struct { 12 | Body io.ReadCloser // underlying Response.Body 13 | zr *gzip.Reader // lazily-initialized gzip reader 14 | zerr error // sticky error 15 | } 16 | 17 | func NewGzipReader(body io.ReadCloser) *GzipReader { 18 | return &GzipReader{Body: body} 19 | } 20 | 21 | func (gz *GzipReader) Read(p []byte) (n int, err error) { 22 | if gz.zerr != nil { 23 | return 0, gz.zerr 24 | } 25 | if gz.zr == nil { 26 | gz.zr, err = gzip.NewReader(gz.Body) 27 | if err != nil { 28 | gz.zerr = err 29 | return 0, err 30 | } 31 | } 32 | return gz.zr.Read(p) 33 | } 34 | 35 | func (gz *GzipReader) Close() error { 36 | if err := gz.Body.Close(); err != nil { 37 | return err 38 | } 39 | gz.zerr = fs.ErrClosed 40 | return nil 41 | } 42 | 43 | func (gz *GzipReader) GetUnderlyingBody() io.ReadCloser { 44 | return gz.Body 45 | } 46 | 47 | func (gz *GzipReader) SetUnderlyingBody(body io.ReadCloser) { 48 | gz.Body = body 49 | } 50 | -------------------------------------------------------------------------------- /internal/compress/reader.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import "io" 4 | 5 | type CompressReader interface { 6 | io.ReadCloser 7 | GetUnderlyingBody() io.ReadCloser 8 | SetUnderlyingBody(body io.ReadCloser) 9 | } 10 | 11 | func NewCompressReader(body io.ReadCloser, contentEncoding string) CompressReader { 12 | switch contentEncoding { 13 | case "gzip": 14 | return NewGzipReader(body) 15 | case "deflate": 16 | return NewDeflateReader(body) 17 | case "br": 18 | return NewBrotliReader(body) 19 | case "zstd": 20 | return NewZstdReader(body) 21 | } 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/compress/zstd_reader.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/klauspost/compress/zstd" 7 | ) 8 | 9 | type ZstdReader struct { 10 | Body io.ReadCloser // underlying Response.Body 11 | zr *zstd.Decoder // lazily-initialized zstd reader 12 | zerr error // sticky error 13 | } 14 | 15 | func NewZstdReader(body io.ReadCloser) *ZstdReader { 16 | return &ZstdReader{Body: body} 17 | } 18 | 19 | func (zr *ZstdReader) Read(p []byte) (n int, err error) { 20 | if zr.zerr != nil { 21 | return 0, zr.zerr 22 | } 23 | if zr.zr == nil { 24 | zr.zr, err = zstd.NewReader(zr.Body) 25 | if err != nil { 26 | zr.zerr = err 27 | return 0, err 28 | } 29 | } 30 | return zr.zr.Read(p) 31 | } 32 | 33 | func (zr *ZstdReader) Close() error { 34 | if zr.zr != nil { 35 | zr.zr.Close() 36 | } 37 | return zr.Body.Close() 38 | } 39 | 40 | func (zr *ZstdReader) GetUnderlyingBody() io.ReadCloser { 41 | return zr.Body 42 | } 43 | 44 | func (zr *ZstdReader) SetUnderlyingBody(body io.ReadCloser) { 45 | zr.Body = body 46 | } 47 | -------------------------------------------------------------------------------- /internal/dump/dump.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | // Options controls the dump behavior. 10 | type Options interface { 11 | Output() io.Writer 12 | RequestHeaderOutput() io.Writer 13 | RequestBodyOutput() io.Writer 14 | ResponseHeaderOutput() io.Writer 15 | ResponseBodyOutput() io.Writer 16 | RequestHeader() bool 17 | RequestBody() bool 18 | ResponseHeader() bool 19 | ResponseBody() bool 20 | Async() bool 21 | Clone() Options 22 | } 23 | 24 | func (d *Dumper) WrapResponseBodyReadCloser(rc io.ReadCloser) io.ReadCloser { 25 | return &dumpResponseBodyReadCloser{rc, d} 26 | } 27 | 28 | type dumpResponseBodyReadCloser struct { 29 | io.ReadCloser 30 | dump *Dumper 31 | } 32 | 33 | func (r *dumpResponseBodyReadCloser) Read(p []byte) (n int, err error) { 34 | n, err = r.ReadCloser.Read(p) 35 | r.dump.DumpResponseBody(p[:n]) 36 | if err == io.EOF { 37 | r.dump.DumpDefault([]byte("\r\n")) 38 | } 39 | return 40 | } 41 | 42 | func (d *Dumper) WrapRequestBodyWriteCloser(rc io.WriteCloser) io.WriteCloser { 43 | return &dumpRequestBodyWriteCloser{rc, d} 44 | } 45 | 46 | type dumpRequestBodyWriteCloser struct { 47 | io.WriteCloser 48 | dump *Dumper 49 | } 50 | 51 | func (w *dumpRequestBodyWriteCloser) Write(p []byte) (n int, err error) { 52 | n, err = w.WriteCloser.Write(p) 53 | w.dump.DumpRequestBody(p[:n]) 54 | return 55 | } 56 | 57 | type dumpRequestHeaderWriter struct { 58 | w io.Writer 59 | dump *Dumper 60 | } 61 | 62 | func (w *dumpRequestHeaderWriter) Write(p []byte) (n int, err error) { 63 | n, err = w.w.Write(p) 64 | w.dump.DumpRequestHeader(p[:n]) 65 | return 66 | } 67 | 68 | func (d *Dumper) WrapRequestHeaderWriter(w io.Writer) io.Writer { 69 | return &dumpRequestHeaderWriter{ 70 | w: w, 71 | dump: d, 72 | } 73 | } 74 | 75 | type dumpRequestBodyWriter struct { 76 | w io.Writer 77 | dump *Dumper 78 | } 79 | 80 | func (w *dumpRequestBodyWriter) Write(p []byte) (n int, err error) { 81 | n, err = w.w.Write(p) 82 | w.dump.DumpRequestBody(p[:n]) 83 | return 84 | } 85 | 86 | func (d *Dumper) WrapRequestBodyWriter(w io.Writer) io.Writer { 87 | return &dumpRequestBodyWriter{ 88 | w: w, 89 | dump: d, 90 | } 91 | } 92 | 93 | // GetResponseHeaderDumpers return Dumpers which need dump response header. 94 | func GetResponseHeaderDumpers(ctx context.Context, dump *Dumper) Dumpers { 95 | dumpers := GetDumpers(ctx, dump) 96 | var ds []*Dumper 97 | for _, d := range dumpers { 98 | if d.ResponseHeader() { 99 | ds = append(ds, d) 100 | } 101 | } 102 | return Dumpers(ds) 103 | } 104 | 105 | // Dumpers is an array of Dumpper 106 | type Dumpers []*Dumper 107 | 108 | // ShouldDump is true if Dumper is not empty. 109 | func (ds Dumpers) ShouldDump() bool { 110 | return len(ds) > 0 111 | } 112 | 113 | func (ds Dumpers) DumpResponseHeader(p []byte) { 114 | for _, d := range ds { 115 | d.DumpResponseHeader(p) 116 | } 117 | } 118 | 119 | // Dumper is the dump tool. 120 | type Dumper struct { 121 | Options 122 | ch chan *dumpTask 123 | } 124 | 125 | type dumpTask struct { 126 | Data []byte 127 | Output io.Writer 128 | } 129 | 130 | // NewDumper create a new Dumper. 131 | func NewDumper(opt Options) *Dumper { 132 | d := &Dumper{ 133 | Options: opt, 134 | ch: make(chan *dumpTask, 20), 135 | } 136 | return d 137 | } 138 | 139 | func (d *Dumper) SetOptions(opt Options) { 140 | d.Options = opt 141 | } 142 | 143 | func (d *Dumper) Clone() *Dumper { 144 | if d == nil { 145 | return nil 146 | } 147 | return &Dumper{ 148 | Options: d.Options.Clone(), 149 | ch: make(chan *dumpTask, 20), 150 | } 151 | } 152 | 153 | func (d *Dumper) DumpTo(p []byte, output io.Writer) { 154 | if len(p) == 0 || output == nil { 155 | return 156 | } 157 | if d.Async() { 158 | b := make([]byte, len(p)) 159 | copy(b, p) 160 | d.ch <- &dumpTask{Data: b, Output: output} 161 | return 162 | } 163 | output.Write(p) 164 | } 165 | 166 | func (d *Dumper) DumpDefault(p []byte) { 167 | d.DumpTo(p, d.Output()) 168 | } 169 | 170 | func (d *Dumper) DumpRequestHeader(p []byte) { 171 | d.DumpTo(p, d.RequestHeaderOutput()) 172 | } 173 | 174 | func (d *Dumper) DumpRequestBody(p []byte) { 175 | d.DumpTo(p, d.RequestBodyOutput()) 176 | } 177 | 178 | func (d *Dumper) DumpResponseHeader(p []byte) { 179 | d.DumpTo(p, d.ResponseHeaderOutput()) 180 | } 181 | 182 | func (d *Dumper) DumpResponseBody(p []byte) { 183 | d.DumpTo(p, d.ResponseBodyOutput()) 184 | } 185 | 186 | func (d *Dumper) Stop() { 187 | d.ch <- nil 188 | } 189 | 190 | func (d *Dumper) Start() { 191 | for t := range d.ch { 192 | if t == nil { 193 | return 194 | } 195 | t.Output.Write(t.Data) 196 | } 197 | } 198 | 199 | type dumperKeyType int 200 | 201 | const DumperKey dumperKeyType = iota 202 | 203 | func GetDumpers(ctx context.Context, dump *Dumper) []*Dumper { 204 | dumps := []*Dumper{} 205 | if dump != nil { 206 | dumps = append(dumps, dump) 207 | } 208 | if ctx == nil { 209 | return dumps 210 | } 211 | if d, ok := ctx.Value(DumperKey).(*Dumper); ok { 212 | dumps = append(dumps, d) 213 | } 214 | return dumps 215 | } 216 | 217 | func WrapResponseBodyIfNeeded(res *http.Response, req *http.Request, dump *Dumper) { 218 | dumps := GetDumpers(req.Context(), dump) 219 | for _, d := range dumps { 220 | if d.ResponseBody() { 221 | res.Body = d.WrapResponseBodyReadCloser(res.Body) 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /internal/godebugs/table.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package godebugs provides a table of known GODEBUG settings, 6 | // for use by a variety of other packages, including internal/godebug, 7 | // runtime, runtime/metrics, and cmd/go/internal/load. 8 | package godebugs 9 | 10 | // An Info describes a single known GODEBUG setting. 11 | type Info struct { 12 | Name string // name of the setting ("panicnil") 13 | Package string // package that uses the setting ("runtime") 14 | Changed int // minor version when default changed, if any; 21 means Go 1.21 15 | Old string // value that restores behavior prior to Changed 16 | Opaque bool // setting does not export information to runtime/metrics using [internal/godebug.Setting.IncNonDefault] 17 | } 18 | 19 | // All is the table of known settings, sorted by Name. 20 | // 21 | // Note: After adding entries to this table, run 'go generate runtime/metrics' 22 | // to update the runtime/metrics doc comment. 23 | // (Otherwise the runtime/metrics test will fail.) 24 | // 25 | // Note: After adding entries to this table, update the list in doc/godebug.md as well. 26 | // (Otherwise the test in this package will fail.) 27 | var All = []Info{ 28 | {Name: "execerrdot", Package: "os/exec"}, 29 | {Name: "gocachehash", Package: "cmd/go"}, 30 | {Name: "gocachetest", Package: "cmd/go"}, 31 | {Name: "gocacheverify", Package: "cmd/go"}, 32 | {Name: "gotypesalias", Package: "go/types"}, 33 | {Name: "http2client", Package: "net/http"}, 34 | {Name: "http2debug", Package: "net/http", Opaque: true}, 35 | {Name: "http2server", Package: "net/http"}, 36 | {Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"}, 37 | {Name: "httpmuxgo121", Package: "net/http", Changed: 22, Old: "1"}, 38 | {Name: "installgoroot", Package: "go/build"}, 39 | {Name: "jstmpllitinterp", Package: "html/template"}, 40 | //{Name: "multipartfiles", Package: "mime/multipart"}, 41 | {Name: "multipartmaxheaders", Package: "mime/multipart"}, 42 | {Name: "multipartmaxparts", Package: "mime/multipart"}, 43 | {Name: "multipathtcp", Package: "net"}, 44 | {Name: "netdns", Package: "net", Opaque: true}, 45 | {Name: "panicnil", Package: "runtime", Changed: 21, Old: "1"}, 46 | {Name: "randautoseed", Package: "math/rand"}, 47 | {Name: "tarinsecurepath", Package: "archive/tar"}, 48 | {Name: "tls10server", Package: "crypto/tls", Changed: 22, Old: "1"}, 49 | {Name: "tlsmaxrsasize", Package: "crypto/tls"}, 50 | {Name: "tlsrsakex", Package: "crypto/tls", Changed: 22, Old: "1"}, 51 | {Name: "tlsunsafeekm", Package: "crypto/tls", Changed: 22, Old: "1"}, 52 | {Name: "winreadlinkvolume", Package: "os", Changed: 22, Old: "0"}, 53 | {Name: "winsymlink", Package: "os", Changed: 22, Old: "0"}, 54 | {Name: "x509sha1", Package: "crypto/x509"}, 55 | {Name: "x509usefallbackroots", Package: "crypto/x509"}, 56 | {Name: "x509usepolicies", Package: "crypto/x509"}, 57 | {Name: "zipinsecurepath", Package: "archive/zip"}, 58 | } 59 | 60 | // Lookup returns the Info with the given name. 61 | func Lookup(name string) *Info { 62 | // binary search, avoiding import of sort. 63 | lo := 0 64 | hi := len(All) 65 | for lo < hi { 66 | m := int(uint(lo+hi) >> 1) 67 | mid := All[m].Name 68 | if name == mid { 69 | return &All[m] 70 | } 71 | if name < mid { 72 | hi = m 73 | } else { 74 | lo = m + 1 75 | } 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/header/header.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import "strings" 4 | 5 | const ( 6 | DefaultUserAgent = "req/v3 (https://github.com/imroc/req)" 7 | UserAgent = "User-Agent" 8 | Location = "Location" 9 | ContentType = "Content-Type" 10 | PlainTextContentType = "text/plain; charset=utf-8" 11 | JsonContentType = "application/json; charset=utf-8" 12 | XmlContentType = "text/xml; charset=utf-8" 13 | FormContentType = "application/x-www-form-urlencoded" 14 | WwwAuthenticate = "WWW-Authenticate" 15 | Authorization = "Authorization" 16 | HeaderOderKey = "__header_order__" 17 | PseudoHeaderOderKey = "__pseudo_header_order__" 18 | ) 19 | 20 | var reqWriteExcludeHeader = map[string]bool{ 21 | // Host is :authority, already sent. 22 | // Content-Length is automatic. 23 | "host": true, 24 | "content-length": true, 25 | // Per 8.1.2.2 Connection-Specific Header 26 | // Fields, don't send connection-specific 27 | // fields. We have already checked if any 28 | // are error-worthy so just ignore the rest. 29 | "connection": true, 30 | "proxy-connection": true, 31 | "transfer-encoding": true, 32 | "upgrade": true, 33 | "keep-alive": true, 34 | // Ignore header order keys which is only used internally. 35 | HeaderOderKey: true, 36 | PseudoHeaderOderKey: true, 37 | } 38 | 39 | func IsExcluded(key string) bool { 40 | if reqWriteExcludeHeader[strings.ToLower(key)] { 41 | return true 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /internal/header/sort.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "net/textproto" 5 | "sort" 6 | ) 7 | 8 | type KeyValues struct { 9 | Key string 10 | Values []string 11 | } 12 | 13 | type sorter struct { 14 | order map[string]int 15 | kvs []KeyValues 16 | } 17 | 18 | func (s *sorter) Len() int { return len(s.kvs) } 19 | func (s *sorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] } 20 | func (s *sorter) Less(i, j int) bool { 21 | if index, ok := s.order[textproto.CanonicalMIMEHeaderKey(s.kvs[i].Key)]; ok { 22 | i = index 23 | } 24 | if index, ok := s.order[textproto.CanonicalMIMEHeaderKey(s.kvs[j].Key)]; ok { 25 | j = index 26 | } 27 | return i < j 28 | } 29 | 30 | func SortKeyValues(kvs []KeyValues, orderedKeys []string) { 31 | order := make(map[string]int) 32 | for i, key := range orderedKeys { 33 | order[textproto.CanonicalMIMEHeaderKey(key)] = i 34 | } 35 | s := &sorter{ 36 | order: order, 37 | kvs: kvs, 38 | } 39 | sort.Sort(s) 40 | } 41 | -------------------------------------------------------------------------------- /internal/http2/databuffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http2 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "sync" 11 | ) 12 | 13 | // Buffer chunks are allocated from a pool to reduce pressure on GC. 14 | // The maximum wasted space per dataBuffer is 2x the largest size class, 15 | // which happens when the dataBuffer has multiple chunks and there is 16 | // one unread byte in both the first and last chunks. We use a few size 17 | // classes to minimize overheads for servers that typically receive very 18 | // small request bodies. 19 | // 20 | // TODO: Benchmark to determine if the pools are necessary. The GC may have 21 | // improved enough that we can instead allocate chunks like this: 22 | // make([]byte, max(16<<10, expectedBytesRemaining)) 23 | var dataChunkPools = [...]sync.Pool{ 24 | {New: func() interface{} { return new([1 << 10]byte) }}, 25 | {New: func() interface{} { return new([2 << 10]byte) }}, 26 | {New: func() interface{} { return new([4 << 10]byte) }}, 27 | {New: func() interface{} { return new([8 << 10]byte) }}, 28 | {New: func() interface{} { return new([16 << 10]byte) }}, 29 | } 30 | 31 | func getDataBufferChunk(size int64) []byte { 32 | switch { 33 | case size <= 1<<10: 34 | return dataChunkPools[0].Get().(*[1 << 10]byte)[:] 35 | case size <= 2<<10: 36 | return dataChunkPools[1].Get().(*[2 << 10]byte)[:] 37 | case size <= 4<<10: 38 | return dataChunkPools[2].Get().(*[4 << 10]byte)[:] 39 | case size <= 8<<10: 40 | return dataChunkPools[3].Get().(*[8 << 10]byte)[:] 41 | default: 42 | return dataChunkPools[4].Get().(*[16 << 10]byte)[:] 43 | } 44 | } 45 | 46 | func putDataBufferChunk(p []byte) { 47 | switch len(p) { 48 | case 1 << 10: 49 | dataChunkPools[0].Put((*[1 << 10]byte)(p)) 50 | case 2 << 10: 51 | dataChunkPools[1].Put((*[2 << 10]byte)(p)) 52 | case 4 << 10: 53 | dataChunkPools[2].Put((*[4 << 10]byte)(p)) 54 | case 8 << 10: 55 | dataChunkPools[3].Put((*[8 << 10]byte)(p)) 56 | case 16 << 10: 57 | dataChunkPools[4].Put((*[16 << 10]byte)(p)) 58 | default: 59 | panic(fmt.Sprintf("unexpected buffer len=%v", len(p))) 60 | } 61 | } 62 | 63 | // dataBuffer is an io.ReadWriter backed by a list of data chunks. 64 | // Each dataBuffer is used to read DATA frames on a single stream. 65 | // The buffer is divided into chunks so the server can limit the 66 | // total memory used by a single connection without limiting the 67 | // request body size on any single stream. 68 | type dataBuffer struct { 69 | chunks [][]byte 70 | r int // next byte to read is chunks[0][r] 71 | w int // next byte to write is chunks[len(chunks)-1][w] 72 | size int // total buffered bytes 73 | expected int64 // we expect at least this many bytes in future Write calls (ignored if <= 0) 74 | } 75 | 76 | var errReadEmpty = errors.New("read from empty dataBuffer") 77 | 78 | // Read copies bytes from the buffer into p. 79 | // It is an error to read when no data is available. 80 | func (b *dataBuffer) Read(p []byte) (int, error) { 81 | if b.size == 0 { 82 | return 0, errReadEmpty 83 | } 84 | var ntotal int 85 | for len(p) > 0 && b.size > 0 { 86 | readFrom := b.bytesFromFirstChunk() 87 | n := copy(p, readFrom) 88 | p = p[n:] 89 | ntotal += n 90 | b.r += n 91 | b.size -= n 92 | // If the first chunk has been consumed, advance to the next chunk. 93 | if b.r == len(b.chunks[0]) { 94 | putDataBufferChunk(b.chunks[0]) 95 | end := len(b.chunks) - 1 96 | copy(b.chunks[:end], b.chunks[1:]) 97 | b.chunks[end] = nil 98 | b.chunks = b.chunks[:end] 99 | b.r = 0 100 | } 101 | } 102 | return ntotal, nil 103 | } 104 | 105 | func (b *dataBuffer) bytesFromFirstChunk() []byte { 106 | if len(b.chunks) == 1 { 107 | return b.chunks[0][b.r:b.w] 108 | } 109 | return b.chunks[0][b.r:] 110 | } 111 | 112 | // Len returns the number of bytes of the unread portion of the buffer. 113 | func (b *dataBuffer) Len() int { 114 | return b.size 115 | } 116 | 117 | // Write appends p to the buffer. 118 | func (b *dataBuffer) Write(p []byte) (int, error) { 119 | ntotal := len(p) 120 | for len(p) > 0 { 121 | // If the last chunk is empty, allocate a new chunk. Try to allocate 122 | // enough to fully copy p plus any additional bytes we expect to 123 | // receive. However, this may allocate less than len(p). 124 | want := int64(len(p)) 125 | if b.expected > want { 126 | want = b.expected 127 | } 128 | chunk := b.lastChunkOrAlloc(want) 129 | n := copy(chunk[b.w:], p) 130 | p = p[n:] 131 | b.w += n 132 | b.size += n 133 | b.expected -= int64(n) 134 | } 135 | return ntotal, nil 136 | } 137 | 138 | func (b *dataBuffer) lastChunkOrAlloc(want int64) []byte { 139 | if len(b.chunks) != 0 { 140 | last := b.chunks[len(b.chunks)-1] 141 | if b.w < len(last) { 142 | return last 143 | } 144 | } 145 | chunk := getDataBufferChunk(want) 146 | b.chunks = append(b.chunks, chunk) 147 | b.w = 0 148 | return chunk 149 | } 150 | -------------------------------------------------------------------------------- /internal/http2/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http2 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // An ErrCode is an unsigned 32-bit error code as defined in the HTTP/2 spec. 13 | type ErrCode uint32 14 | 15 | const ( 16 | ErrCodeNo ErrCode = 0x0 17 | ErrCodeProtocol ErrCode = 0x1 18 | ErrCodeInternal ErrCode = 0x2 19 | ErrCodeFlowControl ErrCode = 0x3 20 | ErrCodeSettingsTimeout ErrCode = 0x4 21 | ErrCodeStreamClosed ErrCode = 0x5 22 | ErrCodeFrameSize ErrCode = 0x6 23 | ErrCodeRefusedStream ErrCode = 0x7 24 | ErrCodeCancel ErrCode = 0x8 25 | ErrCodeCompression ErrCode = 0x9 26 | ErrCodeConnect ErrCode = 0xa 27 | ErrCodeEnhanceYourCalm ErrCode = 0xb 28 | ErrCodeInadequateSecurity ErrCode = 0xc 29 | ErrCodeHTTP11Required ErrCode = 0xd 30 | ) 31 | 32 | var errCodeName = map[ErrCode]string{ 33 | ErrCodeNo: "NO_ERROR", 34 | ErrCodeProtocol: "PROTOCOL_ERROR", 35 | ErrCodeInternal: "INTERNAL_ERROR", 36 | ErrCodeFlowControl: "FLOW_CONTROL_ERROR", 37 | ErrCodeSettingsTimeout: "SETTINGS_TIMEOUT", 38 | ErrCodeStreamClosed: "STREAM_CLOSED", 39 | ErrCodeFrameSize: "FRAME_SIZE_ERROR", 40 | ErrCodeRefusedStream: "REFUSED_STREAM", 41 | ErrCodeCancel: "CANCEL", 42 | ErrCodeCompression: "COMPRESSION_ERROR", 43 | ErrCodeConnect: "CONNECT_ERROR", 44 | ErrCodeEnhanceYourCalm: "ENHANCE_YOUR_CALM", 45 | ErrCodeInadequateSecurity: "INADEQUATE_SECURITY", 46 | ErrCodeHTTP11Required: "HTTP_1_1_REQUIRED", 47 | } 48 | 49 | func (e ErrCode) String() string { 50 | if s, ok := errCodeName[e]; ok { 51 | return s 52 | } 53 | return fmt.Sprintf("unknown error code 0x%x", uint32(e)) 54 | } 55 | 56 | func (e ErrCode) stringToken() string { 57 | if s, ok := errCodeName[e]; ok { 58 | return s 59 | } 60 | return fmt.Sprintf("ERR_UNKNOWN_%d", uint32(e)) 61 | } 62 | 63 | // ConnectionError is an error that results in the termination of the 64 | // entire connection. 65 | type ConnectionError ErrCode 66 | 67 | func (e ConnectionError) Error() string { 68 | return fmt.Sprintf("connection error: %s", ErrCode(e)) 69 | } 70 | 71 | // StreamError is an error that only affects one stream within an 72 | // HTTP/2 connection. 73 | type StreamError struct { 74 | StreamID uint32 75 | Code ErrCode 76 | Cause error // optional additional detail 77 | } 78 | 79 | // errFromPeer is a sentinel error value for StreamError.Cause to 80 | // indicate that the StreamError was sent from the peer over the wire 81 | // and wasn't locally generated in the Transport. 82 | var errFromPeer = errors.New("received from peer") 83 | 84 | func streamError(id uint32, code ErrCode) StreamError { 85 | return StreamError{StreamID: id, Code: code} 86 | } 87 | 88 | func (e StreamError) Error() string { 89 | if e.Cause != nil { 90 | return fmt.Sprintf("stream error: stream ID %d; %v; %v", e.StreamID, e.Code, e.Cause) 91 | } 92 | return fmt.Sprintf("stream error: stream ID %d; %v", e.StreamID, e.Code) 93 | } 94 | 95 | // connError represents an HTTP/2 ConnectionError error code, along 96 | // with a string (for debugging) explaining why. 97 | // 98 | // Errors of this type are only returned by the frame parser functions 99 | // and converted into ConnectionError(Code), after stashing away 100 | // the Reason into the Framer's errDetail field, accessible via 101 | // the (*Framer).ErrorDetail method. 102 | type connError struct { 103 | Code ErrCode // the ConnectionError error code 104 | Reason string // additional reason 105 | } 106 | 107 | func (e connError) Error() string { 108 | return fmt.Sprintf("http2: connection error: %v: %v", e.Code, e.Reason) 109 | } 110 | 111 | type pseudoHeaderError string 112 | 113 | func (e pseudoHeaderError) Error() string { 114 | return fmt.Sprintf("invalid pseudo-header %q", string(e)) 115 | } 116 | 117 | type duplicatePseudoHeaderError string 118 | 119 | func (e duplicatePseudoHeaderError) Error() string { 120 | return fmt.Sprintf("duplicate pseudo-header %q", string(e)) 121 | } 122 | 123 | type headerFieldNameError string 124 | 125 | func (e headerFieldNameError) Error() string { 126 | return fmt.Sprintf("invalid header field name %q", string(e)) 127 | } 128 | 129 | type headerFieldValueError string 130 | 131 | func (e headerFieldValueError) Error() string { 132 | return fmt.Sprintf("invalid header field value for %q", string(e)) 133 | } 134 | 135 | var ( 136 | errMixPseudoHeaderTypes = errors.New("mix of request and response pseudo headers") 137 | errPseudoAfterRegular = errors.New("pseudo header field after regular") 138 | ) 139 | -------------------------------------------------------------------------------- /internal/http2/flow.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Flow control 6 | 7 | package http2 8 | 9 | // inflowMinRefresh is the minimum number of bytes we'll send for a 10 | // flow control window update. 11 | const inflowMinRefresh = 4 << 10 12 | 13 | // inflow accounts for an inbound flow control window. 14 | // It tracks both the latest window sent to the peer (used for enforcement) 15 | // and the accumulated unsent window. 16 | type inflow struct { 17 | avail int32 18 | unsent int32 19 | } 20 | 21 | // init sets the initial window. 22 | func (f *inflow) init(n int32) { 23 | f.avail = n 24 | } 25 | 26 | // add adds n bytes to the window, with a maximum window size of max, 27 | // indicating that the peer can now send us more data. 28 | // For example, the user read from a {Request,Response} body and consumed 29 | // some of the buffered data, so the peer can now send more. 30 | // It returns the number of bytes to send in a WINDOW_UPDATE frame to the peer. 31 | // Window updates are accumulated and sent when the unsent capacity 32 | // is at least inflowMinRefresh or will at least double the peer's available window. 33 | func (f *inflow) add(n int) (connAdd int32) { 34 | if n < 0 { 35 | panic("negative update") 36 | } 37 | unsent := int64(f.unsent) + int64(n) 38 | // "A sender MUST NOT allow a flow-control window to exceed 2^31-1 octets." 39 | // RFC 7540 Section 6.9.1. 40 | const maxWindow = 1<<31 - 1 41 | if unsent+int64(f.avail) > maxWindow { 42 | panic("flow control update exceeds maximum window size") 43 | } 44 | f.unsent = int32(unsent) 45 | if f.unsent < inflowMinRefresh && f.unsent < f.avail { 46 | // If there aren't at least inflowMinRefresh bytes of window to send, 47 | // and this update won't at least double the window, buffer the update for later. 48 | return 0 49 | } 50 | f.avail += f.unsent 51 | f.unsent = 0 52 | return int32(unsent) 53 | } 54 | 55 | // take attempts to take n bytes from the peer's flow control window. 56 | // It reports whether the window has available capacity. 57 | func (f *inflow) take(n uint32) bool { 58 | if n > uint32(f.avail) { 59 | return false 60 | } 61 | f.avail -= int32(n) 62 | return true 63 | } 64 | 65 | // takeInflows attempts to take n bytes from two inflows, 66 | // typically connection-level and stream-level flows. 67 | // It reports whether both windows have available capacity. 68 | func takeInflows(f1, f2 *inflow, n uint32) bool { 69 | if n > uint32(f1.avail) || n > uint32(f2.avail) { 70 | return false 71 | } 72 | f1.avail -= int32(n) 73 | f2.avail -= int32(n) 74 | return true 75 | } 76 | 77 | // outflow is the outbound flow control window's size. 78 | type outflow struct { 79 | _ incomparable 80 | 81 | // n is the number of DATA bytes we're allowed to send. 82 | // An outflow is kept both on a conn and a per-stream. 83 | n int32 84 | 85 | // conn points to the shared connection-level outflow that is 86 | // shared by all streams on that conn. It is nil for the outflow 87 | // that's on the conn directly. 88 | conn *outflow 89 | } 90 | 91 | func (f *outflow) setConnFlow(cf *outflow) { f.conn = cf } 92 | 93 | func (f *outflow) available() int32 { 94 | n := f.n 95 | if f.conn != nil && f.conn.n < n { 96 | n = f.conn.n 97 | } 98 | return n 99 | } 100 | 101 | func (f *outflow) take(n int32) { 102 | if n > f.available() { 103 | panic("internal error: took too much") 104 | } 105 | f.n -= n 106 | if f.conn != nil { 107 | f.conn.n -= n 108 | } 109 | } 110 | 111 | // add adds n bytes (positive or negative) to the flow control window. 112 | // It returns false if the sum would exceed 2^31-1. 113 | func (f *outflow) add(n int32) bool { 114 | sum := f.n + n 115 | if (sum > n) == (f.n > 0) { 116 | f.n = sum 117 | return true 118 | } 119 | return false 120 | } 121 | -------------------------------------------------------------------------------- /internal/http2/gotrack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Defensive debug-only utility to track that functions run on the 6 | // goroutine that they're supposed to. 7 | 8 | package http2 9 | 10 | import ( 11 | "bytes" 12 | "errors" 13 | "fmt" 14 | "os" 15 | "runtime" 16 | "strconv" 17 | "sync" 18 | ) 19 | 20 | var DebugGoroutines = os.Getenv("DEBUG_HTTP2_GOROUTINES") == "1" 21 | 22 | type goroutineLock uint64 23 | 24 | func newGoroutineLock() goroutineLock { 25 | if !DebugGoroutines { 26 | return 0 27 | } 28 | return goroutineLock(curGoroutineID()) 29 | } 30 | 31 | func (g goroutineLock) check() { 32 | if !DebugGoroutines { 33 | return 34 | } 35 | if curGoroutineID() != uint64(g) { 36 | panic("running on the wrong goroutine") 37 | } 38 | } 39 | 40 | func (g goroutineLock) checkNotOn() { 41 | if !DebugGoroutines { 42 | return 43 | } 44 | if curGoroutineID() == uint64(g) { 45 | panic("running on the wrong goroutine") 46 | } 47 | } 48 | 49 | var goroutineSpace = []byte("goroutine ") 50 | 51 | func curGoroutineID() uint64 { 52 | bp := littleBuf.Get().(*[]byte) 53 | defer littleBuf.Put(bp) 54 | b := *bp 55 | b = b[:runtime.Stack(b, false)] 56 | // Parse the 4707 out of "goroutine 4707 [" 57 | b = bytes.TrimPrefix(b, goroutineSpace) 58 | i := bytes.IndexByte(b, ' ') 59 | if i < 0 { 60 | panic(fmt.Sprintf("No space found in %q", b)) 61 | } 62 | b = b[:i] 63 | n, err := parseUintBytes(b, 10, 64) 64 | if err != nil { 65 | panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err)) 66 | } 67 | return n 68 | } 69 | 70 | var littleBuf = sync.Pool{ 71 | New: func() interface{} { 72 | buf := make([]byte, 64) 73 | return &buf 74 | }, 75 | } 76 | 77 | // parseUintBytes is like strconv.ParseUint, but using a []byte. 78 | func parseUintBytes(s []byte, base int, bitSize int) (n uint64, err error) { 79 | var cutoff, maxVal uint64 80 | 81 | if bitSize == 0 { 82 | bitSize = int(strconv.IntSize) 83 | } 84 | 85 | s0 := s 86 | switch { 87 | case len(s) < 1: 88 | err = strconv.ErrSyntax 89 | goto Error 90 | 91 | case 2 <= base && base <= 36: 92 | // valid base; nothing to do 93 | 94 | case base == 0: 95 | // Look for octal, hex prefix. 96 | switch { 97 | case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): 98 | base = 16 99 | s = s[2:] 100 | if len(s) < 1 { 101 | err = strconv.ErrSyntax 102 | goto Error 103 | } 104 | case s[0] == '0': 105 | base = 8 106 | default: 107 | base = 10 108 | } 109 | 110 | default: 111 | err = errors.New("invalid base " + strconv.Itoa(base)) 112 | goto Error 113 | } 114 | 115 | n = 0 116 | cutoff = cutoff64(base) 117 | maxVal = 1<= base { 135 | n = 0 136 | err = strconv.ErrSyntax 137 | goto Error 138 | } 139 | 140 | if n >= cutoff { 141 | // n*base overflows 142 | n = 1<<64 - 1 143 | err = strconv.ErrRange 144 | goto Error 145 | } 146 | n *= uint64(base) 147 | 148 | n1 := n + uint64(v) 149 | if n1 < n || n1 > maxVal { 150 | // n+v overflows 151 | n = 1<<64 - 1 152 | err = strconv.ErrRange 153 | goto Error 154 | } 155 | n = n1 156 | } 157 | 158 | return n, nil 159 | 160 | Error: 161 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} 162 | } 163 | 164 | // Return the first number n such that n*base >= 1<<64. 165 | func cutoff64(base int) uint64 { 166 | if base < 2 { 167 | return 0 168 | } 169 | return (1<<64-1)/uint64(base) + 1 170 | } 171 | -------------------------------------------------------------------------------- /internal/http2/headermap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http2 6 | 7 | import ( 8 | "github.com/imroc/req/v3/internal/ascii" 9 | "net/http" 10 | "sync" 11 | ) 12 | 13 | var ( 14 | commonBuildOnce sync.Once 15 | commonLowerHeader map[string]string // Go-Canonical-Case -> lower-case 16 | commonCanonHeader map[string]string // lower-case -> Go-Canonical-Case 17 | ) 18 | 19 | func buildCommonHeaderMapsOnce() { 20 | commonBuildOnce.Do(buildCommonHeaderMaps) 21 | } 22 | 23 | func buildCommonHeaderMaps() { 24 | common := []string{ 25 | "accept", 26 | "accept-charset", 27 | "accept-encoding", 28 | "accept-language", 29 | "accept-ranges", 30 | "age", 31 | "access-control-allow-credentials", 32 | "access-control-allow-headers", 33 | "access-control-allow-methods", 34 | "access-control-allow-origin", 35 | "access-control-expose-headers", 36 | "access-control-max-age", 37 | "access-control-request-headers", 38 | "access-control-request-method", 39 | "allow", 40 | "authorization", 41 | "cache-control", 42 | "content-disposition", 43 | "content-encoding", 44 | "content-language", 45 | "content-length", 46 | "content-location", 47 | "content-range", 48 | "content-type", 49 | "cookie", 50 | "date", 51 | "etag", 52 | "expect", 53 | "expires", 54 | "from", 55 | "host", 56 | "if-match", 57 | "if-modified-since", 58 | "if-none-match", 59 | "if-unmodified-since", 60 | "last-modified", 61 | "link", 62 | "location", 63 | "max-forwards", 64 | "origin", 65 | "proxy-authenticate", 66 | "proxy-authorization", 67 | "range", 68 | "referer", 69 | "refresh", 70 | "retry-after", 71 | "server", 72 | "set-cookie", 73 | "strict-transport-security", 74 | "trailer", 75 | "transfer-encoding", 76 | "user-agent", 77 | "vary", 78 | "via", 79 | "www-authenticate", 80 | "x-forwarded-for", 81 | "x-forwarded-proto", 82 | } 83 | commonLowerHeader = make(map[string]string, len(common)) 84 | commonCanonHeader = make(map[string]string, len(common)) 85 | for _, v := range common { 86 | chk := http.CanonicalHeaderKey(v) 87 | commonLowerHeader[chk] = v 88 | commonCanonHeader[v] = chk 89 | } 90 | } 91 | 92 | func lowerHeader(v string) (lower string, isAscii bool) { 93 | buildCommonHeaderMapsOnce() 94 | if s, ok := commonLowerHeader[v]; ok { 95 | return s, true 96 | } 97 | return ascii.ToLower(v) 98 | } 99 | 100 | func canonicalHeader(v string) string { 101 | buildCommonHeaderMapsOnce() 102 | if s, ok := commonCanonHeader[v]; ok { 103 | return s 104 | } 105 | return http.CanonicalHeaderKey(v) 106 | } 107 | -------------------------------------------------------------------------------- /internal/http2/http2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http2 6 | 7 | import ( 8 | "bufio" 9 | "crypto/tls" 10 | "net/http" 11 | "os" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | 17 | "golang.org/x/net/http/httpguts" 18 | ) 19 | 20 | var ( 21 | VerboseLogs bool 22 | logFrameWrites bool 23 | logFrameReads bool 24 | inTests bool 25 | ) 26 | 27 | func init() { 28 | e := os.Getenv("GODEBUG") 29 | if strings.Contains(e, "http2debug=1") { 30 | VerboseLogs = true 31 | } 32 | if strings.Contains(e, "http2debug=2") { 33 | VerboseLogs = true 34 | logFrameWrites = true 35 | logFrameReads = true 36 | } 37 | } 38 | 39 | const ( 40 | // ClientPreface is the string that must be sent by new 41 | // connections from clients. 42 | ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" 43 | 44 | // NextProtoTLS is the NPN/ALPN protocol negotiated during 45 | // HTTP/2's TLS setup. 46 | NextProtoTLS = "h2" 47 | 48 | // https://httpwg.org/specs/rfc7540.html#SettingValues 49 | initialHeaderTableSize = 4096 50 | 51 | initialWindowSize = 65535 // 6.9.2 Initial Flow Control Window Size 52 | ) 53 | 54 | var clientPreface = []byte(ClientPreface) 55 | 56 | // validWireHeaderFieldName reports whether v is a valid header field 57 | // name (key). See httpguts.ValidHeaderName for the base rules. 58 | // 59 | // Further, http2 says: 60 | // 61 | // "Just as in HTTP/1.x, header field names are strings of ASCII 62 | // characters that are compared in a case-insensitive 63 | // fashion. However, header field names MUST be converted to 64 | // lowercase prior to their encoding in HTTP/2. " 65 | func validWireHeaderFieldName(v string) bool { 66 | if len(v) == 0 { 67 | return false 68 | } 69 | for _, r := range v { 70 | if !httpguts.IsTokenRune(r) { 71 | return false 72 | } 73 | if 'A' <= r && r <= 'Z' { 74 | return false 75 | } 76 | } 77 | return true 78 | } 79 | 80 | func httpCodeString(code int) string { 81 | switch code { 82 | case 200: 83 | return "200" 84 | case 404: 85 | return "404" 86 | } 87 | return strconv.Itoa(code) 88 | } 89 | 90 | // bufWriterPoolBufferSize is the size of bufio.Writer's 91 | // buffers created using bufWriterPool. 92 | // 93 | // TODO: pick a less arbitrary value? this is a bit under 94 | // (3 x typical 1500 byte MTU) at least. Other than that, 95 | // not much thought went into it. 96 | const bufWriterPoolBufferSize = 4 << 10 97 | 98 | var bufWriterPool = sync.Pool{ 99 | New: func() interface{} { 100 | return bufio.NewWriterSize(nil, bufWriterPoolBufferSize) 101 | }, 102 | } 103 | 104 | func mustUint31(v int32) uint32 { 105 | if v < 0 || v > 2147483647 { 106 | panic("out of range") 107 | } 108 | return uint32(v) 109 | } 110 | 111 | // bodyAllowedForStatus reports whether a given response status code 112 | // permits a body. See RFC 7230, section 3.3. 113 | func bodyAllowedForStatus(status int) bool { 114 | switch { 115 | case status >= 100 && status <= 199: 116 | return false 117 | case status == 204: 118 | return false 119 | case status == 304: 120 | return false 121 | } 122 | return true 123 | } 124 | 125 | type httpError struct { 126 | _ incomparable 127 | msg string 128 | timeout bool 129 | } 130 | 131 | func (e *httpError) Error() string { return e.msg } 132 | 133 | func (e *httpError) Timeout() bool { return e.timeout } 134 | 135 | func (e *httpError) Temporary() bool { return true } 136 | 137 | var errH2Timeout error = &httpError{msg: "http2: timeout awaiting response headers", timeout: true} 138 | 139 | type connectionStater interface { 140 | ConnectionState() tls.ConnectionState 141 | } 142 | 143 | var sorterPool = sync.Pool{New: func() interface{} { return new(sorter) }} 144 | 145 | type sorter struct { 146 | v []string // owned by sorter 147 | } 148 | 149 | func (s *sorter) Len() int { return len(s.v) } 150 | 151 | func (s *sorter) Swap(i, j int) { s.v[i], s.v[j] = s.v[j], s.v[i] } 152 | 153 | func (s *sorter) Less(i, j int) bool { return s.v[i] < s.v[j] } 154 | 155 | // Keys returns the sorted keys of h. 156 | // 157 | // The returned slice is only valid until s used again or returned to 158 | // its pool. 159 | func (s *sorter) Keys(h http.Header) []string { 160 | keys := s.v[:0] 161 | for k := range h { 162 | keys = append(keys, k) 163 | } 164 | s.v = keys 165 | sort.Sort(s) 166 | return keys 167 | } 168 | 169 | func (s *sorter) SortStrings(ss []string) { 170 | // Our sorter works on s.v, which sorter owns, so 171 | // stash it away while we sort the user's buffer. 172 | save := s.v 173 | s.v = ss 174 | sort.Sort(s) 175 | s.v = save 176 | } 177 | 178 | // validPseudoPath reports whether v is a valid :path pseudo-header 179 | // value. It must be either: 180 | // 181 | // *) a non-empty string starting with '/' 182 | // *) the string '*', for OPTIONS requests. 183 | // 184 | // For now this is only used a quick check for deciding when to clean 185 | // up Opaque URLs before sending requests from the Transport. 186 | // See golang.org/issue/16847 187 | // 188 | // We used to enforce that the path also didn't start with "//", but 189 | // Google's GFE accepts such paths and Chrome sends them, so ignore 190 | // that part of the spec. See golang.org/issue/19103. 191 | func validPseudoPath(v string) bool { 192 | return (len(v) > 0 && v[0] == '/') || v == "*" 193 | } 194 | 195 | // incomparable is a zero-width, non-comparable type. Adding it to a struct 196 | // makes that struct also non-comparable, and generally doesn't add 197 | // any size (as long as it's first). 198 | type incomparable [0]func() 199 | -------------------------------------------------------------------------------- /internal/http2/pipe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http2 6 | 7 | import ( 8 | "errors" 9 | "io" 10 | "sync" 11 | ) 12 | 13 | // pipe is a goroutine-safe io.Reader/io.Writer pair. It's like 14 | // io.Pipe except there are no PipeReader/PipeWriter halves, and the 15 | // underlying buffer is an interface. (io.Pipe is always unbuffered) 16 | type pipe struct { 17 | mu sync.Mutex 18 | c sync.Cond // c.L lazily initialized to &p.mu 19 | b pipeBuffer // nil when done reading 20 | unread int // bytes unread when done 21 | err error // read error once empty. non-nil means closed. 22 | breakErr error // immediate read error (caller doesn't see rest of b) 23 | donec chan struct{} // closed on error 24 | readFn func() // optional code to run in Read before error 25 | } 26 | 27 | type pipeBuffer interface { 28 | Len() int 29 | io.Writer 30 | io.Reader 31 | } 32 | 33 | // setBuffer initializes the pipe buffer. 34 | // It has no effect if the pipe is already closed. 35 | func (p *pipe) setBuffer(b pipeBuffer) { 36 | p.mu.Lock() 37 | defer p.mu.Unlock() 38 | if p.err != nil || p.breakErr != nil { 39 | return 40 | } 41 | p.b = b 42 | } 43 | 44 | func (p *pipe) Len() int { 45 | p.mu.Lock() 46 | defer p.mu.Unlock() 47 | if p.b == nil { 48 | return p.unread 49 | } 50 | return p.b.Len() 51 | } 52 | 53 | // Read waits until data is available and copies bytes 54 | // from the buffer into p. 55 | func (p *pipe) Read(d []byte) (n int, err error) { 56 | p.mu.Lock() 57 | defer p.mu.Unlock() 58 | if p.c.L == nil { 59 | p.c.L = &p.mu 60 | } 61 | for { 62 | if p.breakErr != nil { 63 | return 0, p.breakErr 64 | } 65 | if p.b != nil && p.b.Len() > 0 { 66 | return p.b.Read(d) 67 | } 68 | if p.err != nil { 69 | if p.readFn != nil { 70 | p.readFn() // e.g. copy trailers 71 | p.readFn = nil // not sticky like p.err 72 | } 73 | p.b = nil 74 | return 0, p.err 75 | } 76 | p.c.Wait() 77 | } 78 | } 79 | 80 | var ( 81 | errClosedPipeWrite = errors.New("write on closed buffer") 82 | errUninitializedPipeWrite = errors.New("write on uninitialized buffer") 83 | ) 84 | 85 | // Write copies bytes from p into the buffer and wakes a reader. 86 | // It is an error to write more data than the buffer can hold. 87 | func (p *pipe) Write(d []byte) (n int, err error) { 88 | p.mu.Lock() 89 | defer p.mu.Unlock() 90 | if p.c.L == nil { 91 | p.c.L = &p.mu 92 | } 93 | defer p.c.Signal() 94 | if p.err != nil || p.breakErr != nil { 95 | return 0, errClosedPipeWrite 96 | } 97 | // pipe.setBuffer is never invoked, leaving the buffer uninitialized. 98 | // We shouldn't try to write to an uninitialized pipe, 99 | // but returning an error is better than panicking. 100 | if p.b == nil { 101 | return 0, errUninitializedPipeWrite 102 | } 103 | return p.b.Write(d) 104 | } 105 | 106 | // CloseWithError causes the next Read (waking up a current blocked 107 | // Read if needed) to return the provided err after all data has been 108 | // read. 109 | // 110 | // The error must be non-nil. 111 | func (p *pipe) CloseWithError(err error) { p.closeWithError(&p.err, err, nil) } 112 | 113 | // BreakWithError causes the next Read (waking up a current blocked 114 | // Read if needed) to return the provided err immediately, without 115 | // waiting for unread data. 116 | func (p *pipe) BreakWithError(err error) { p.closeWithError(&p.breakErr, err, nil) } 117 | 118 | // closeWithErrorAndCode is like CloseWithError but also sets some code to run 119 | // in the caller's goroutine before returning the error. 120 | func (p *pipe) closeWithErrorAndCode(err error, fn func()) { p.closeWithError(&p.err, err, fn) } 121 | 122 | func (p *pipe) closeWithError(dst *error, err error, fn func()) { 123 | if err == nil { 124 | panic("err must be non-nil") 125 | } 126 | p.mu.Lock() 127 | defer p.mu.Unlock() 128 | if p.c.L == nil { 129 | p.c.L = &p.mu 130 | } 131 | defer p.c.Signal() 132 | if *dst != nil { 133 | // Already been done. 134 | return 135 | } 136 | p.readFn = fn 137 | if dst == &p.breakErr { 138 | if p.b != nil { 139 | p.unread += p.b.Len() 140 | } 141 | p.b = nil 142 | } 143 | *dst = err 144 | p.closeDoneLocked() 145 | } 146 | 147 | // requires p.mu be held. 148 | func (p *pipe) closeDoneLocked() { 149 | if p.donec == nil { 150 | return 151 | } 152 | // Close if unclosed. This isn't racy since we always 153 | // hold p.mu while closing. 154 | select { 155 | case <-p.donec: 156 | default: 157 | close(p.donec) 158 | } 159 | } 160 | 161 | // Err returns the error (if any) first set by BreakWithError or CloseWithError. 162 | func (p *pipe) Err() error { 163 | p.mu.Lock() 164 | defer p.mu.Unlock() 165 | if p.breakErr != nil { 166 | return p.breakErr 167 | } 168 | return p.err 169 | } 170 | 171 | // Done returns a channel which is closed if and when this pipe is closed 172 | // with CloseWithError. 173 | func (p *pipe) Done() <-chan struct{} { 174 | p.mu.Lock() 175 | defer p.mu.Unlock() 176 | if p.donec == nil { 177 | p.donec = make(chan struct{}) 178 | if p.err != nil || p.breakErr != nil { 179 | // Already hit an error. 180 | p.closeDoneLocked() 181 | } 182 | } 183 | return p.donec 184 | } 185 | -------------------------------------------------------------------------------- /internal/http2/timer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | package http2 5 | 6 | import "time" 7 | 8 | // A timer is a time.Timer, as an interface which can be replaced in tests. 9 | type timer = interface { 10 | C() <-chan time.Time 11 | Reset(d time.Duration) bool 12 | Stop() bool 13 | } 14 | 15 | // timeTimer adapts a time.Timer to the timer interface. 16 | type timeTimer struct { 17 | *time.Timer 18 | } 19 | 20 | func (t timeTimer) C() <-chan time.Time { return t.Timer.C } 21 | -------------------------------------------------------------------------------- /internal/http2/trace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http2 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptrace" 10 | "net/textproto" 11 | "time" 12 | ) 13 | 14 | func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { 15 | return trace != nil && trace.WroteHeaderField != nil 16 | } 17 | 18 | func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) { 19 | if trace != nil && trace.WroteHeaderField != nil { 20 | trace.WroteHeaderField(k, []string{v}) 21 | } 22 | } 23 | 24 | func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { 25 | if trace != nil { 26 | return trace.Got1xxResponse 27 | } 28 | return nil 29 | } 30 | 31 | func traceGetConn(req *http.Request, hostPort string) { 32 | trace := httptrace.ContextClientTrace(req.Context()) 33 | if trace == nil || trace.GetConn == nil { 34 | return 35 | } 36 | trace.GetConn(hostPort) 37 | } 38 | 39 | func traceGotConn(req *http.Request, cc *ClientConn, reused bool) { 40 | trace := httptrace.ContextClientTrace(req.Context()) 41 | if trace == nil || trace.GotConn == nil { 42 | return 43 | } 44 | ci := httptrace.GotConnInfo{Conn: cc.tconn} 45 | ci.Reused = reused 46 | cc.mu.Lock() 47 | ci.WasIdle = len(cc.streams) == 0 && reused 48 | if ci.WasIdle && !cc.lastActive.IsZero() { 49 | ci.IdleTime = time.Now().Sub(cc.lastActive) 50 | } 51 | cc.mu.Unlock() 52 | 53 | trace.GotConn(ci) 54 | } 55 | 56 | func traceWroteHeaders(trace *httptrace.ClientTrace) { 57 | if trace != nil && trace.WroteHeaders != nil { 58 | trace.WroteHeaders() 59 | } 60 | } 61 | 62 | func traceGot100Continue(trace *httptrace.ClientTrace) { 63 | if trace != nil && trace.Got100Continue != nil { 64 | trace.Got100Continue() 65 | } 66 | } 67 | 68 | func traceWait100Continue(trace *httptrace.ClientTrace) { 69 | if trace != nil && trace.Wait100Continue != nil { 70 | trace.Wait100Continue() 71 | } 72 | } 73 | 74 | func traceWroteRequest(trace *httptrace.ClientTrace, err error) { 75 | if trace != nil && trace.WroteRequest != nil { 76 | trace.WroteRequest(httptrace.WroteRequestInfo{Err: err}) 77 | } 78 | } 79 | 80 | func traceFirstResponseByte(trace *httptrace.ClientTrace) { 81 | if trace != nil && trace.GotFirstResponseByte != nil { 82 | trace.GotFirstResponseByte() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/http3/body.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "sync" 8 | 9 | "github.com/quic-go/quic-go" 10 | ) 11 | 12 | // A Hijacker allows hijacking of the stream creating part of a quic.Session from a http.Response.Body. 13 | // It is used by WebTransport to create WebTransport streams after a session has been established. 14 | type Hijacker interface { 15 | Connection() Connection 16 | } 17 | 18 | var errTooMuchData = errors.New("peer sent too much data") 19 | 20 | // The body is used in the requestBody (for a http.Request) and the responseBody (for a http.Response). 21 | type body struct { 22 | str *stream 23 | 24 | remainingContentLength int64 25 | violatedContentLength bool 26 | hasContentLength bool 27 | } 28 | 29 | func newBody(str *stream, contentLength int64) *body { 30 | b := &body{str: str} 31 | if contentLength >= 0 { 32 | b.hasContentLength = true 33 | b.remainingContentLength = contentLength 34 | } 35 | return b 36 | } 37 | 38 | func (r *body) StreamID() quic.StreamID { return r.str.StreamID() } 39 | 40 | func (r *body) checkContentLengthViolation() error { 41 | if !r.hasContentLength { 42 | return nil 43 | } 44 | if r.remainingContentLength < 0 || r.remainingContentLength == 0 && r.str.hasMoreData() { 45 | if !r.violatedContentLength { 46 | r.str.CancelRead(quic.StreamErrorCode(ErrCodeMessageError)) 47 | r.str.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError)) 48 | r.violatedContentLength = true 49 | } 50 | return errTooMuchData 51 | } 52 | return nil 53 | } 54 | 55 | func (r *body) Read(b []byte) (int, error) { 56 | if err := r.checkContentLengthViolation(); err != nil { 57 | return 0, err 58 | } 59 | if r.hasContentLength { 60 | b = b[:min(int64(len(b)), r.remainingContentLength)] 61 | } 62 | n, err := r.str.Read(b) 63 | r.remainingContentLength -= int64(n) 64 | if err := r.checkContentLengthViolation(); err != nil { 65 | return n, err 66 | } 67 | return n, maybeReplaceError(err) 68 | } 69 | 70 | func (r *body) Close() error { 71 | r.str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled)) 72 | return nil 73 | } 74 | 75 | type requestBody struct { 76 | body 77 | connCtx context.Context 78 | rcvdSettings <-chan struct{} 79 | getSettings func() *Settings 80 | } 81 | 82 | var _ io.ReadCloser = &requestBody{} 83 | 84 | func newRequestBody(str *stream, contentLength int64, connCtx context.Context, rcvdSettings <-chan struct{}, getSettings func() *Settings) *requestBody { 85 | return &requestBody{ 86 | body: *newBody(str, contentLength), 87 | connCtx: connCtx, 88 | rcvdSettings: rcvdSettings, 89 | getSettings: getSettings, 90 | } 91 | } 92 | 93 | type hijackableBody struct { 94 | body body 95 | 96 | // only set for the http.Response 97 | // The channel is closed when the user is done with this response: 98 | // either when Read() errors, or when Close() is called. 99 | reqDone chan<- struct{} 100 | reqDoneOnce sync.Once 101 | } 102 | 103 | var _ io.ReadCloser = &hijackableBody{} 104 | 105 | func newResponseBody(str *stream, contentLength int64, done chan<- struct{}) *hijackableBody { 106 | return &hijackableBody{ 107 | body: *newBody(str, contentLength), 108 | reqDone: done, 109 | } 110 | } 111 | 112 | func (r *hijackableBody) Read(b []byte) (int, error) { 113 | n, err := r.body.Read(b) 114 | if err != nil { 115 | r.requestDone() 116 | } 117 | return n, maybeReplaceError(err) 118 | } 119 | 120 | func (r *hijackableBody) requestDone() { 121 | if r.reqDone != nil { 122 | r.reqDoneOnce.Do(func() { 123 | close(r.reqDone) 124 | }) 125 | } 126 | } 127 | 128 | func (r *hijackableBody) Close() error { 129 | r.requestDone() 130 | // If the EOF was read, CancelRead() is a no-op. 131 | r.body.str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled)) 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /internal/http3/capsule.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import "io" 4 | 5 | type countingByteReader struct { 6 | io.ByteReader 7 | Read int 8 | } 9 | 10 | func (r *countingByteReader) ReadByte() (byte, error) { 11 | b, err := r.ByteReader.ReadByte() 12 | if err == nil { 13 | r.Read++ 14 | } 15 | return b, err 16 | } 17 | -------------------------------------------------------------------------------- /internal/http3/datagram.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | const maxQuarterStreamID = 1<<60 - 1 9 | 10 | const streamDatagramQueueLen = 32 11 | 12 | type datagrammer struct { 13 | sendDatagram func([]byte) error 14 | 15 | hasData chan struct{} 16 | queue [][]byte // TODO: use a ring buffer 17 | 18 | mx sync.Mutex 19 | sendErr error 20 | receiveErr error 21 | } 22 | 23 | func newDatagrammer(sendDatagram func([]byte) error) *datagrammer { 24 | return &datagrammer{ 25 | sendDatagram: sendDatagram, 26 | hasData: make(chan struct{}, 1), 27 | } 28 | } 29 | 30 | func (d *datagrammer) SetReceiveError(err error) { 31 | d.mx.Lock() 32 | defer d.mx.Unlock() 33 | 34 | d.receiveErr = err 35 | d.signalHasData() 36 | } 37 | 38 | func (d *datagrammer) SetSendError(err error) { 39 | d.mx.Lock() 40 | defer d.mx.Unlock() 41 | 42 | d.sendErr = err 43 | } 44 | 45 | func (d *datagrammer) Send(b []byte) error { 46 | d.mx.Lock() 47 | sendErr := d.sendErr 48 | d.mx.Unlock() 49 | if sendErr != nil { 50 | return sendErr 51 | } 52 | 53 | return d.sendDatagram(b) 54 | } 55 | 56 | func (d *datagrammer) signalHasData() { 57 | select { 58 | case d.hasData <- struct{}{}: 59 | default: 60 | } 61 | } 62 | 63 | func (d *datagrammer) enqueue(data []byte) { 64 | d.mx.Lock() 65 | defer d.mx.Unlock() 66 | 67 | if d.receiveErr != nil { 68 | return 69 | } 70 | if len(d.queue) >= streamDatagramQueueLen { 71 | return 72 | } 73 | d.queue = append(d.queue, data) 74 | d.signalHasData() 75 | } 76 | 77 | func (d *datagrammer) Receive(ctx context.Context) ([]byte, error) { 78 | start: 79 | d.mx.Lock() 80 | if len(d.queue) >= 1 { 81 | data := d.queue[0] 82 | d.queue = d.queue[1:] 83 | d.mx.Unlock() 84 | return data, nil 85 | } 86 | if receiveErr := d.receiveErr; receiveErr != nil { 87 | d.mx.Unlock() 88 | return nil, receiveErr 89 | } 90 | d.mx.Unlock() 91 | 92 | select { 93 | case <-ctx.Done(): 94 | return nil, context.Cause(ctx) 95 | case <-d.hasData: 96 | } 97 | goto start 98 | } 99 | -------------------------------------------------------------------------------- /internal/http3/error.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/quic-go/quic-go" 8 | ) 9 | 10 | // Error is returned from the round tripper (for HTTP clients) 11 | // and inside the HTTP handler (for HTTP servers) if an HTTP/3 error occurs. 12 | // See section 8 of RFC 9114. 13 | type Error struct { 14 | Remote bool 15 | ErrorCode ErrCode 16 | ErrorMessage string 17 | } 18 | 19 | var _ error = &Error{} 20 | 21 | func (e *Error) Error() string { 22 | s := e.ErrorCode.string() 23 | if s == "" { 24 | s = fmt.Sprintf("H3 error (%#x)", uint64(e.ErrorCode)) 25 | } 26 | // Usually errors are remote. Only make it explicit for local errors. 27 | if !e.Remote { 28 | s += " (local)" 29 | } 30 | if e.ErrorMessage != "" { 31 | s += ": " + e.ErrorMessage 32 | } 33 | return s 34 | } 35 | 36 | func (e *Error) Is(target error) bool { 37 | t, ok := target.(*Error) 38 | return ok && e.ErrorCode == t.ErrorCode && e.Remote == t.Remote 39 | } 40 | 41 | func maybeReplaceError(err error) error { 42 | if err == nil { 43 | return nil 44 | } 45 | 46 | var ( 47 | e Error 48 | strErr *quic.StreamError 49 | appErr *quic.ApplicationError 50 | ) 51 | switch { 52 | default: 53 | return err 54 | case errors.As(err, &strErr): 55 | e.Remote = strErr.Remote 56 | e.ErrorCode = ErrCode(strErr.ErrorCode) 57 | case errors.As(err, &appErr): 58 | e.Remote = appErr.Remote 59 | e.ErrorCode = ErrCode(appErr.ErrorCode) 60 | e.ErrorMessage = appErr.ErrorMessage 61 | } 62 | return &e 63 | } 64 | -------------------------------------------------------------------------------- /internal/http3/error_codes.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/quic-go/quic-go" 7 | ) 8 | 9 | type ErrCode quic.ApplicationErrorCode 10 | 11 | const ( 12 | ErrCodeNoError ErrCode = 0x100 13 | ErrCodeGeneralProtocolError ErrCode = 0x101 14 | ErrCodeInternalError ErrCode = 0x102 15 | ErrCodeStreamCreationError ErrCode = 0x103 16 | ErrCodeClosedCriticalStream ErrCode = 0x104 17 | ErrCodeFrameUnexpected ErrCode = 0x105 18 | ErrCodeFrameError ErrCode = 0x106 19 | ErrCodeExcessiveLoad ErrCode = 0x107 20 | ErrCodeIDError ErrCode = 0x108 21 | ErrCodeSettingsError ErrCode = 0x109 22 | ErrCodeMissingSettings ErrCode = 0x10a 23 | ErrCodeRequestRejected ErrCode = 0x10b 24 | ErrCodeRequestCanceled ErrCode = 0x10c 25 | ErrCodeRequestIncomplete ErrCode = 0x10d 26 | ErrCodeMessageError ErrCode = 0x10e 27 | ErrCodeConnectError ErrCode = 0x10f 28 | ErrCodeVersionFallback ErrCode = 0x110 29 | ErrCodeDatagramError ErrCode = 0x33 30 | ) 31 | 32 | func (e ErrCode) String() string { 33 | s := e.string() 34 | if s != "" { 35 | return s 36 | } 37 | return fmt.Sprintf("unknown error code: %#x", uint16(e)) 38 | } 39 | 40 | func (e ErrCode) string() string { 41 | switch e { 42 | case ErrCodeNoError: 43 | return "H3_NO_ERROR" 44 | case ErrCodeGeneralProtocolError: 45 | return "H3_GENERAL_PROTOCOL_ERROR" 46 | case ErrCodeInternalError: 47 | return "H3_INTERNAL_ERROR" 48 | case ErrCodeStreamCreationError: 49 | return "H3_STREAM_CREATION_ERROR" 50 | case ErrCodeClosedCriticalStream: 51 | return "H3_CLOSED_CRITICAL_STREAM" 52 | case ErrCodeFrameUnexpected: 53 | return "H3_FRAME_UNEXPECTED" 54 | case ErrCodeFrameError: 55 | return "H3_FRAME_ERROR" 56 | case ErrCodeExcessiveLoad: 57 | return "H3_EXCESSIVE_LOAD" 58 | case ErrCodeIDError: 59 | return "H3_ID_ERROR" 60 | case ErrCodeSettingsError: 61 | return "H3_SETTINGS_ERROR" 62 | case ErrCodeMissingSettings: 63 | return "H3_MISSING_SETTINGS" 64 | case ErrCodeRequestRejected: 65 | return "H3_REQUEST_REJECTED" 66 | case ErrCodeRequestCanceled: 67 | return "H3_REQUEST_CANCELLED" 68 | case ErrCodeRequestIncomplete: 69 | return "H3_INCOMPLETE_REQUEST" 70 | case ErrCodeMessageError: 71 | return "H3_MESSAGE_ERROR" 72 | case ErrCodeConnectError: 73 | return "H3_CONNECT_ERROR" 74 | case ErrCodeVersionFallback: 75 | return "H3_VERSION_FALLBACK" 76 | case ErrCodeDatagramError: 77 | return "H3_DATAGRAM_ERROR" 78 | default: 79 | return "" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/http3/ip_addr.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | // An addrList represents a list of network endpoint addresses. 9 | // Copy from [net.addrList] and change type from [net.Addr] to [net.IPAddr] 10 | type addrList []net.IPAddr 11 | 12 | // isIPv4 reports whether addr contains an IPv4 address. 13 | func isIPv4(addr net.IPAddr) bool { 14 | return addr.IP.To4() != nil 15 | } 16 | 17 | // isNotIPv4 reports whether addr does not contain an IPv4 address. 18 | func isNotIPv4(addr net.IPAddr) bool { return !isIPv4(addr) } 19 | 20 | // forResolve returns the most appropriate address in address for 21 | // a call to ResolveTCPAddr, ResolveUDPAddr, or ResolveIPAddr. 22 | // IPv4 is preferred, unless addr contains an IPv6 literal. 23 | func (addrs addrList) forResolve(network, addr string) net.IPAddr { 24 | var want6 bool 25 | switch network { 26 | case "ip": 27 | // IPv6 literal (addr does NOT contain a port) 28 | want6 = strings.ContainsRune(addr, ':') 29 | case "tcp", "udp": 30 | // IPv6 literal. (addr contains a port, so look for '[') 31 | want6 = strings.ContainsRune(addr, '[') 32 | } 33 | if want6 { 34 | return addrs.first(isNotIPv4) 35 | } 36 | return addrs.first(isIPv4) 37 | } 38 | 39 | // first returns the first address which satisfies strategy, or if 40 | // none do, then the first address of any kind. 41 | func (addrs addrList) first(strategy func(net.IPAddr) bool) net.IPAddr { 42 | for _, addr := range addrs { 43 | if strategy(addr) { 44 | return addr 45 | } 46 | } 47 | return addrs[0] 48 | } 49 | -------------------------------------------------------------------------------- /internal/http3/protocol.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/quic-go/quic-go" 7 | ) 8 | 9 | // Perspective determines if we're acting as a server or a client 10 | type Perspective int 11 | 12 | // the perspectives 13 | const ( 14 | PerspectiveServer Perspective = 1 15 | PerspectiveClient Perspective = 2 16 | ) 17 | 18 | // Opposite returns the perspective of the peer 19 | func (p Perspective) Opposite() Perspective { 20 | return 3 - p 21 | } 22 | 23 | func (p Perspective) String() string { 24 | switch p { 25 | case PerspectiveServer: 26 | return "server" 27 | case PerspectiveClient: 28 | return "client" 29 | default: 30 | return "invalid perspective" 31 | } 32 | } 33 | 34 | // The version numbers, making grepping easier 35 | const ( 36 | VersionUnknown quic.Version = math.MaxUint32 37 | versionDraft29 quic.Version = 0xff00001d // draft-29 used to be a widely deployed version 38 | Version1 quic.Version = 0x1 39 | Version2 quic.Version = 0x6b3343cf 40 | ) 41 | 42 | // SupportedVersions lists the versions that the server supports 43 | // must be in sorted descending order 44 | var SupportedVersions = []quic.Version{Version1, Version2} 45 | 46 | // StreamType encodes if this is a unidirectional or bidirectional stream 47 | type StreamType uint8 48 | 49 | const ( 50 | // StreamTypeUni is a unidirectional stream 51 | StreamTypeUni StreamType = iota 52 | // StreamTypeBidi is a bidirectional stream 53 | StreamTypeBidi 54 | ) 55 | 56 | // A StreamID in QUIC 57 | type StreamID int64 58 | 59 | // InitiatedBy says if the stream was initiated by the client or by the server 60 | func (s StreamID) InitiatedBy() Perspective { 61 | if s%2 == 0 { 62 | return PerspectiveClient 63 | } 64 | return PerspectiveServer 65 | } 66 | 67 | // Type says if this is a unidirectional or bidirectional stream 68 | func (s StreamID) Type() StreamType { 69 | if s%4 >= 2 { 70 | return StreamTypeUni 71 | } 72 | return StreamTypeBidi 73 | } 74 | 75 | // StreamNum returns how many streams in total are below this 76 | // Example: for stream 9 it returns 3 (i.e. streams 1, 5 and 9) 77 | func (s StreamID) StreamNum() StreamNum { 78 | return StreamNum(s/4) + 1 79 | } 80 | 81 | // InvalidPacketNumber is a stream ID that is invalid. 82 | // The first valid stream ID in QUIC is 0. 83 | const InvalidStreamID StreamID = -1 84 | 85 | // StreamNum is the stream number 86 | type StreamNum int64 87 | 88 | const ( 89 | // InvalidStreamNum is an invalid stream number. 90 | InvalidStreamNum = -1 91 | // MaxStreamCount is the maximum stream count value that can be sent in MAX_STREAMS frames 92 | // and as the stream count in the transport parameters 93 | MaxStreamCount StreamNum = 1 << 60 94 | ) 95 | 96 | // StreamID calculates the stream ID. 97 | func (s StreamNum) StreamID(stype StreamType, pers Perspective) StreamID { 98 | if s == 0 { 99 | return InvalidStreamID 100 | } 101 | var first StreamID 102 | switch stype { 103 | case StreamTypeBidi: 104 | switch pers { 105 | case PerspectiveClient: 106 | first = 0 107 | case PerspectiveServer: 108 | first = 1 109 | } 110 | case StreamTypeUni: 111 | switch pers { 112 | case PerspectiveClient: 113 | first = 2 114 | case PerspectiveServer: 115 | first = 3 116 | } 117 | } 118 | return first + 4*StreamID(s-1) 119 | } 120 | -------------------------------------------------------------------------------- /internal/http3/server.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import "github.com/quic-go/quic-go" 4 | 5 | // NextProtoH3 is the ALPN protocol negotiated during the TLS handshake, for QUIC v1 and v2. 6 | const NextProtoH3 = "h3" 7 | 8 | // StreamType is the stream type of a unidirectional stream. 9 | type ServerStreamType uint64 10 | 11 | const ( 12 | streamTypeControlStream = 0 13 | streamTypePushStream = 1 14 | streamTypeQPACKEncoderStream = 2 15 | streamTypeQPACKDecoderStream = 3 16 | ) 17 | 18 | func versionToALPN(v quic.Version) string { 19 | //nolint:exhaustive // These are all the versions we care about. 20 | switch v { 21 | case Version1, Version2: 22 | return NextProtoH3 23 | default: 24 | return "" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/http3/state_tracking_stream.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | "sync" 8 | 9 | "github.com/quic-go/quic-go" 10 | ) 11 | 12 | var _ quic.Stream = &stateTrackingStream{} 13 | 14 | // stateTrackingStream is an implementation of quic.Stream that delegates 15 | // to an underlying stream 16 | // it takes care of proxying send and receive errors onto an implementation of 17 | // the errorSetter interface (intended to be occupied by a datagrammer) 18 | // it is also responsible for clearing the stream based on its ID from its 19 | // parent connection, this is done through the streamClearer interface when 20 | // both the send and receive sides are closed 21 | type stateTrackingStream struct { 22 | quic.Stream 23 | 24 | mx sync.Mutex 25 | sendErr error 26 | recvErr error 27 | 28 | clearer streamClearer 29 | setter errorSetter 30 | } 31 | 32 | type streamClearer interface { 33 | clearStream(quic.StreamID) 34 | } 35 | 36 | type errorSetter interface { 37 | SetSendError(error) 38 | SetReceiveError(error) 39 | } 40 | 41 | func newStateTrackingStream(s quic.Stream, clearer streamClearer, setter errorSetter) *stateTrackingStream { 42 | t := &stateTrackingStream{ 43 | Stream: s, 44 | clearer: clearer, 45 | setter: setter, 46 | } 47 | 48 | context.AfterFunc(s.Context(), func() { 49 | t.closeSend(context.Cause(s.Context())) 50 | }) 51 | 52 | return t 53 | } 54 | 55 | func (s *stateTrackingStream) closeSend(e error) { 56 | s.mx.Lock() 57 | defer s.mx.Unlock() 58 | 59 | // clear the stream the first time both the send 60 | // and receive are finished 61 | if s.sendErr == nil { 62 | if s.recvErr != nil { 63 | s.clearer.clearStream(s.StreamID()) 64 | } 65 | 66 | s.setter.SetSendError(e) 67 | s.sendErr = e 68 | } 69 | } 70 | 71 | func (s *stateTrackingStream) closeReceive(e error) { 72 | s.mx.Lock() 73 | defer s.mx.Unlock() 74 | 75 | // clear the stream the first time both the send 76 | // and receive are finished 77 | if s.recvErr == nil { 78 | if s.sendErr != nil { 79 | s.clearer.clearStream(s.StreamID()) 80 | } 81 | 82 | s.setter.SetReceiveError(e) 83 | s.recvErr = e 84 | } 85 | } 86 | 87 | func (s *stateTrackingStream) Close() error { 88 | s.closeSend(errors.New("write on closed stream")) 89 | return s.Stream.Close() 90 | } 91 | 92 | func (s *stateTrackingStream) CancelWrite(e quic.StreamErrorCode) { 93 | s.closeSend(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e}) 94 | s.Stream.CancelWrite(e) 95 | } 96 | 97 | func (s *stateTrackingStream) Write(b []byte) (int, error) { 98 | n, err := s.Stream.Write(b) 99 | if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) { 100 | s.closeSend(err) 101 | } 102 | return n, err 103 | } 104 | 105 | func (s *stateTrackingStream) CancelRead(e quic.StreamErrorCode) { 106 | s.closeReceive(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e}) 107 | s.Stream.CancelRead(e) 108 | } 109 | 110 | func (s *stateTrackingStream) Read(b []byte) (int, error) { 111 | n, err := s.Stream.Read(b) 112 | if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) { 113 | s.closeReceive(err) 114 | } 115 | return n, err 116 | } 117 | -------------------------------------------------------------------------------- /internal/http3/trace.go: -------------------------------------------------------------------------------- 1 | package http3 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http/httptrace" 7 | "net/textproto" 8 | "time" 9 | 10 | "github.com/quic-go/quic-go" 11 | ) 12 | 13 | func traceGetConn(trace *httptrace.ClientTrace, hostPort string) { 14 | if trace != nil && trace.GetConn != nil { 15 | trace.GetConn(hostPort) 16 | } 17 | } 18 | 19 | // fakeConn is a wrapper for quic.EarlyConnection 20 | // because the quic connection does not implement net.Conn. 21 | type fakeConn struct { 22 | conn quic.EarlyConnection 23 | } 24 | 25 | func (c *fakeConn) Close() error { panic("connection operation prohibited") } 26 | func (c *fakeConn) Read(p []byte) (int, error) { panic("connection operation prohibited") } 27 | func (c *fakeConn) Write(p []byte) (int, error) { panic("connection operation prohibited") } 28 | func (c *fakeConn) SetDeadline(t time.Time) error { panic("connection operation prohibited") } 29 | func (c *fakeConn) SetReadDeadline(t time.Time) error { panic("connection operation prohibited") } 30 | func (c *fakeConn) SetWriteDeadline(t time.Time) error { panic("connection operation prohibited") } 31 | func (c *fakeConn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() } 32 | func (c *fakeConn) LocalAddr() net.Addr { return c.conn.LocalAddr() } 33 | 34 | func traceGotConn(trace *httptrace.ClientTrace, conn quic.EarlyConnection, reused bool) { 35 | if trace != nil && trace.GotConn != nil { 36 | trace.GotConn(httptrace.GotConnInfo{ 37 | Conn: &fakeConn{conn: conn}, 38 | Reused: reused, 39 | }) 40 | } 41 | } 42 | 43 | func traceGotFirstResponseByte(trace *httptrace.ClientTrace) { 44 | if trace != nil && trace.GotFirstResponseByte != nil { 45 | trace.GotFirstResponseByte() 46 | } 47 | } 48 | 49 | func traceGot1xxResponse(trace *httptrace.ClientTrace, code int, header textproto.MIMEHeader) { 50 | if trace != nil && trace.Got1xxResponse != nil { 51 | trace.Got1xxResponse(code, header) 52 | } 53 | } 54 | 55 | func traceGot100Continue(trace *httptrace.ClientTrace) { 56 | if trace != nil && trace.Got100Continue != nil { 57 | trace.Got100Continue() 58 | } 59 | } 60 | 61 | func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { 62 | return trace != nil && trace.WroteHeaderField != nil 63 | } 64 | 65 | func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) { 66 | if trace != nil && trace.WroteHeaderField != nil { 67 | trace.WroteHeaderField(k, []string{v}) 68 | } 69 | } 70 | 71 | func traceWroteHeaders(trace *httptrace.ClientTrace) { 72 | if trace != nil && trace.WroteHeaders != nil { 73 | trace.WroteHeaders() 74 | } 75 | } 76 | 77 | func traceWroteRequest(trace *httptrace.ClientTrace, err error) { 78 | if trace != nil && trace.WroteRequest != nil { 79 | trace.WroteRequest(httptrace.WroteRequestInfo{Err: err}) 80 | } 81 | } 82 | 83 | func traceConnectStart(trace *httptrace.ClientTrace, network, addr string) { 84 | if trace != nil && trace.ConnectStart != nil { 85 | trace.ConnectStart(network, addr) 86 | } 87 | } 88 | 89 | func traceConnectDone(trace *httptrace.ClientTrace, network, addr string, err error) { 90 | if trace != nil && trace.ConnectDone != nil { 91 | trace.ConnectDone(network, addr, err) 92 | } 93 | } 94 | 95 | func traceTLSHandshakeStart(trace *httptrace.ClientTrace) { 96 | if trace != nil && trace.TLSHandshakeStart != nil { 97 | trace.TLSHandshakeStart() 98 | } 99 | } 100 | 101 | func traceTLSHandshakeDone(trace *httptrace.ClientTrace, state tls.ConnectionState, err error) { 102 | if trace != nil && trace.TLSHandshakeDone != nil { 103 | trace.TLSHandshakeDone(state, err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/netutil/addr.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "golang.org/x/net/idna" 5 | "net" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | func AuthorityKey(u *url.URL) string { 11 | return u.Scheme + "://" + AuthorityAddr(u.Scheme, u.Host) 12 | } 13 | 14 | // AuthorityAddr returns a given authority (a host/IP, or host:port / ip:port) 15 | // and returns a host:port. The port 443 is added if needed. 16 | func AuthorityAddr(scheme, authority string) (addr string) { 17 | host, port := AuthorityHostPort(scheme, authority) 18 | // IPv6 address literal, without a port: 19 | if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { 20 | return host + ":" + port 21 | } 22 | addr = net.JoinHostPort(host, port) 23 | return 24 | } 25 | 26 | func AuthorityHostPort(scheme, authority string) (host, port string) { 27 | host, port, err := net.SplitHostPort(authority) 28 | if err != nil { // authority didn't have a port 29 | port = "443" 30 | if scheme == "http" { 31 | port = "80" 32 | } 33 | host = authority 34 | } 35 | if a, err := idna.ToASCII(host); err == nil { 36 | host = a 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /internal/quic-go/quicvarint/io.go: -------------------------------------------------------------------------------- 1 | package quicvarint 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // Reader implements both the io.ByteReader and io.Reader interfaces. 9 | type Reader interface { 10 | io.ByteReader 11 | io.Reader 12 | } 13 | 14 | var _ Reader = &bytes.Reader{} 15 | 16 | type byteReader struct { 17 | io.Reader 18 | } 19 | 20 | var _ Reader = &byteReader{} 21 | 22 | // NewReader returns a Reader for r. 23 | // If r already implements both io.ByteReader and io.Reader, NewReader returns r. 24 | // Otherwise, r is wrapped to add the missing interfaces. 25 | func NewReader(r io.Reader) Reader { 26 | if r, ok := r.(Reader); ok { 27 | return r 28 | } 29 | return &byteReader{r} 30 | } 31 | 32 | func (r *byteReader) ReadByte() (byte, error) { 33 | var b [1]byte 34 | n, err := r.Reader.Read(b[:]) 35 | if n == 1 && err == io.EOF { 36 | err = nil 37 | } 38 | return b[0], err 39 | } 40 | 41 | // Writer implements both the io.ByteWriter and io.Writer interfaces. 42 | type Writer interface { 43 | io.ByteWriter 44 | io.Writer 45 | } 46 | 47 | var _ Writer = &bytes.Buffer{} 48 | 49 | type byteWriter struct { 50 | io.Writer 51 | } 52 | 53 | var _ Writer = &byteWriter{} 54 | 55 | // NewWriter returns a Writer for w. 56 | // If r already implements both io.ByteWriter and io.Writer, NewWriter returns w. 57 | // Otherwise, w is wrapped to add the missing interfaces. 58 | func NewWriter(w io.Writer) Writer { 59 | if w, ok := w.(Writer); ok { 60 | return w 61 | } 62 | return &byteWriter{w} 63 | } 64 | 65 | func (w *byteWriter) WriteByte(c byte) error { 66 | _, err := w.Writer.Write([]byte{c}) 67 | return err 68 | } 69 | -------------------------------------------------------------------------------- /internal/quic-go/quicvarint/varint.go: -------------------------------------------------------------------------------- 1 | package quicvarint 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // taken from the QUIC draft 9 | const ( 10 | // Min is the minimum value allowed for a QUIC varint. 11 | Min = 0 12 | 13 | // Max is the maximum allowed value for a QUIC varint (2^62-1). 14 | Max = maxVarInt8 15 | 16 | maxVarInt1 = 63 17 | maxVarInt2 = 16383 18 | maxVarInt4 = 1073741823 19 | maxVarInt8 = 4611686018427387903 20 | ) 21 | 22 | // Read reads a number in the QUIC varint format from r. 23 | func Read(r io.ByteReader) (uint64, error) { 24 | firstByte, err := r.ReadByte() 25 | if err != nil { 26 | return 0, err 27 | } 28 | // the first two bits of the first byte encode the length 29 | l := 1 << ((firstByte & 0xc0) >> 6) 30 | b1 := firstByte & (0xff - 0xc0) 31 | if l == 1 { 32 | return uint64(b1), nil 33 | } 34 | b2, err := r.ReadByte() 35 | if err != nil { 36 | return 0, err 37 | } 38 | if l == 2 { 39 | return uint64(b2) + uint64(b1)<<8, nil 40 | } 41 | b3, err := r.ReadByte() 42 | if err != nil { 43 | return 0, err 44 | } 45 | b4, err := r.ReadByte() 46 | if err != nil { 47 | return 0, err 48 | } 49 | if l == 4 { 50 | return uint64(b4) + uint64(b3)<<8 + uint64(b2)<<16 + uint64(b1)<<24, nil 51 | } 52 | b5, err := r.ReadByte() 53 | if err != nil { 54 | return 0, err 55 | } 56 | b6, err := r.ReadByte() 57 | if err != nil { 58 | return 0, err 59 | } 60 | b7, err := r.ReadByte() 61 | if err != nil { 62 | return 0, err 63 | } 64 | b8, err := r.ReadByte() 65 | if err != nil { 66 | return 0, err 67 | } 68 | return uint64(b8) + uint64(b7)<<8 + uint64(b6)<<16 + uint64(b5)<<24 + uint64(b4)<<32 + uint64(b3)<<40 + uint64(b2)<<48 + uint64(b1)<<56, nil 69 | } 70 | 71 | // Parse reads a number in the QUIC varint format. 72 | // It returns the number of bytes consumed. 73 | func Parse(b []byte) (uint64 /* value */, int /* bytes consumed */, error) { 74 | if len(b) == 0 { 75 | return 0, 0, io.EOF 76 | } 77 | firstByte := b[0] 78 | // the first two bits of the first byte encode the length 79 | l := 1 << ((firstByte & 0xc0) >> 6) 80 | if len(b) < l { 81 | return 0, 0, io.ErrUnexpectedEOF 82 | } 83 | b0 := firstByte & (0xff - 0xc0) 84 | if l == 1 { 85 | return uint64(b0), 1, nil 86 | } 87 | if l == 2 { 88 | return uint64(b[1]) + uint64(b0)<<8, 2, nil 89 | } 90 | if l == 4 { 91 | return uint64(b[3]) + uint64(b[2])<<8 + uint64(b[1])<<16 + uint64(b0)<<24, 4, nil 92 | } 93 | return uint64(b[7]) + uint64(b[6])<<8 + uint64(b[5])<<16 + uint64(b[4])<<24 + uint64(b[3])<<32 + uint64(b[2])<<40 + uint64(b[1])<<48 + uint64(b0)<<56, 8, nil 94 | } 95 | 96 | // Append appends i in the QUIC varint format. 97 | func Append(b []byte, i uint64) []byte { 98 | if i <= maxVarInt1 { 99 | return append(b, uint8(i)) 100 | } 101 | if i <= maxVarInt2 { 102 | return append(b, []byte{uint8(i>>8) | 0x40, uint8(i)}...) 103 | } 104 | if i <= maxVarInt4 { 105 | return append(b, []byte{uint8(i>>24) | 0x80, uint8(i >> 16), uint8(i >> 8), uint8(i)}...) 106 | } 107 | if i <= maxVarInt8 { 108 | return append(b, []byte{ 109 | uint8(i>>56) | 0xc0, uint8(i >> 48), uint8(i >> 40), uint8(i >> 32), 110 | uint8(i >> 24), uint8(i >> 16), uint8(i >> 8), uint8(i), 111 | }...) 112 | } 113 | panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i)) 114 | } 115 | 116 | // AppendWithLen append i in the QUIC varint format with the desired length. 117 | func AppendWithLen(b []byte, i uint64, length int) []byte { 118 | if length != 1 && length != 2 && length != 4 && length != 8 { 119 | panic("invalid varint length") 120 | } 121 | l := Len(i) 122 | if l == length { 123 | return Append(b, i) 124 | } 125 | if l > length { 126 | panic(fmt.Sprintf("cannot encode %d in %d bytes", i, length)) 127 | } 128 | if length == 2 { 129 | b = append(b, 0b01000000) 130 | } else if length == 4 { 131 | b = append(b, 0b10000000) 132 | } else if length == 8 { 133 | b = append(b, 0b11000000) 134 | } 135 | for j := 1; j < length-l; j++ { 136 | b = append(b, 0) 137 | } 138 | for j := 0; j < l; j++ { 139 | b = append(b, uint8(i>>(8*(l-1-j)))) 140 | } 141 | return b 142 | } 143 | 144 | // Len determines the number of bytes that will be needed to write the number i. 145 | func Len(i uint64) int { 146 | if i <= maxVarInt1 { 147 | return 1 148 | } 149 | if i <= maxVarInt2 { 150 | return 2 151 | } 152 | if i <= maxVarInt4 { 153 | return 4 154 | } 155 | if i <= maxVarInt8 { 156 | return 8 157 | } 158 | // Don't use a fmt.Sprintf here to format the error message. 159 | // The function would then exceed the inlining budget. 160 | panic(struct { 161 | message string 162 | num uint64 163 | }{"value doesn't fit into 62 bits: ", i}) 164 | } 165 | -------------------------------------------------------------------------------- /internal/socks/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package socks 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "io" 11 | "net" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | var ( 17 | noDeadline = time.Time{} 18 | aLongTimeAgo = time.Unix(1, 0) 19 | ) 20 | 21 | func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net.Addr, ctxErr error) { 22 | host, port, err := splitHostPort(address) 23 | if err != nil { 24 | return nil, err 25 | } 26 | if deadline, ok := ctx.Deadline(); ok && !deadline.IsZero() { 27 | c.SetDeadline(deadline) 28 | defer c.SetDeadline(noDeadline) 29 | } 30 | if ctx != context.Background() { 31 | errCh := make(chan error, 1) 32 | done := make(chan struct{}) 33 | defer func() { 34 | close(done) 35 | if ctxErr == nil { 36 | ctxErr = <-errCh 37 | } 38 | }() 39 | go func() { 40 | select { 41 | case <-ctx.Done(): 42 | c.SetDeadline(aLongTimeAgo) 43 | errCh <- ctx.Err() 44 | case <-done: 45 | errCh <- nil 46 | } 47 | }() 48 | } 49 | 50 | b := make([]byte, 0, 6+len(host)) // the size here is just an estimate 51 | b = append(b, Version5) 52 | if len(d.AuthMethods) == 0 || d.Authenticate == nil { 53 | b = append(b, 1, byte(AuthMethodNotRequired)) 54 | } else { 55 | ams := d.AuthMethods 56 | if len(ams) > 255 { 57 | return nil, errors.New("too many authentication methods") 58 | } 59 | b = append(b, byte(len(ams))) 60 | for _, am := range ams { 61 | b = append(b, byte(am)) 62 | } 63 | } 64 | if _, ctxErr = c.Write(b); ctxErr != nil { 65 | return 66 | } 67 | 68 | if _, ctxErr = io.ReadFull(c, b[:2]); ctxErr != nil { 69 | return 70 | } 71 | if b[0] != Version5 { 72 | return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) 73 | } 74 | am := AuthMethod(b[1]) 75 | if am == AuthMethodNoAcceptableMethods { 76 | return nil, errors.New("no acceptable authentication methods") 77 | } 78 | if d.Authenticate != nil { 79 | if ctxErr = d.Authenticate(ctx, c, am); ctxErr != nil { 80 | return 81 | } 82 | } 83 | 84 | b = b[:0] 85 | b = append(b, Version5, byte(d.cmd), 0) 86 | if ip := net.ParseIP(host); ip != nil { 87 | if ip4 := ip.To4(); ip4 != nil { 88 | b = append(b, AddrTypeIPv4) 89 | b = append(b, ip4...) 90 | } else if ip6 := ip.To16(); ip6 != nil { 91 | b = append(b, AddrTypeIPv6) 92 | b = append(b, ip6...) 93 | } else { 94 | return nil, errors.New("unknown address type") 95 | } 96 | } else { 97 | if len(host) > 255 { 98 | return nil, errors.New("FQDN too long") 99 | } 100 | b = append(b, AddrTypeFQDN) 101 | b = append(b, byte(len(host))) 102 | b = append(b, host...) 103 | } 104 | b = append(b, byte(port>>8), byte(port)) 105 | if _, ctxErr = c.Write(b); ctxErr != nil { 106 | return 107 | } 108 | 109 | if _, ctxErr = io.ReadFull(c, b[:4]); ctxErr != nil { 110 | return 111 | } 112 | if b[0] != Version5 { 113 | return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) 114 | } 115 | if cmdErr := Reply(b[1]); cmdErr != StatusSucceeded { 116 | return nil, errors.New("unknown error " + cmdErr.String()) 117 | } 118 | if b[2] != 0 { 119 | return nil, errors.New("non-zero reserved field") 120 | } 121 | l := 2 122 | var a Addr 123 | switch b[3] { 124 | case AddrTypeIPv4: 125 | l += net.IPv4len 126 | a.IP = make(net.IP, net.IPv4len) 127 | case AddrTypeIPv6: 128 | l += net.IPv6len 129 | a.IP = make(net.IP, net.IPv6len) 130 | case AddrTypeFQDN: 131 | if _, err := io.ReadFull(c, b[:1]); err != nil { 132 | return nil, err 133 | } 134 | l += int(b[0]) 135 | default: 136 | return nil, errors.New("unknown address type " + strconv.Itoa(int(b[3]))) 137 | } 138 | if cap(b) < l { 139 | b = make([]byte, l) 140 | } else { 141 | b = b[:l] 142 | } 143 | if _, ctxErr = io.ReadFull(c, b); ctxErr != nil { 144 | return 145 | } 146 | if a.IP != nil { 147 | copy(a.IP, b) 148 | } else { 149 | a.Name = string(b[:len(b)-2]) 150 | } 151 | a.Port = int(b[len(b)-2])<<8 | int(b[len(b)-1]) 152 | return &a, nil 153 | } 154 | 155 | func splitHostPort(address string) (string, int, error) { 156 | host, port, err := net.SplitHostPort(address) 157 | if err != nil { 158 | return "", 0, err 159 | } 160 | portnum, err := strconv.Atoi(port) 161 | if err != nil { 162 | return "", 0, err 163 | } 164 | if 1 > portnum || portnum > 0xffff { 165 | return "", 0, errors.New("port number out of range " + port) 166 | } 167 | return host, portnum, nil 168 | } 169 | -------------------------------------------------------------------------------- /internal/socks/socks_test.go: -------------------------------------------------------------------------------- 1 | package socks 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "github.com/imroc/req/v3/internal/tests" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestReply(t *testing.T) { 12 | for i := 0; i < 9; i++ { 13 | s := Reply(i).String() 14 | if strings.Contains(s, "unknown") { 15 | t.Errorf("resply code [%d] should not unknown", i) 16 | } 17 | } 18 | s := Reply(9).String() 19 | if !strings.Contains(s, "unknown") { 20 | t.Errorf("resply code [%d] should unknown", 9) 21 | } 22 | } 23 | 24 | func TestAuthenticate(t *testing.T) { 25 | auth := &UsernamePassword{ 26 | Username: "imroc", 27 | Password: "123456", 28 | } 29 | buf := bytes.NewBuffer([]byte{byte(0x01), byte(0x00)}) 30 | err := auth.Authenticate(context.Background(), buf, AuthMethodUsernamePassword) 31 | tests.AssertNoError(t, err) 32 | auth.Username = "this is a very long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long name" 33 | err = auth.Authenticate(context.Background(), buf, AuthMethodUsernamePassword) 34 | tests.AssertErrorContains(t, err, "invalid") 35 | 36 | auth.Username = "imroc" 37 | buf = bytes.NewBuffer([]byte{byte(0x03), byte(0x00)}) 38 | err = auth.Authenticate(context.Background(), buf, AuthMethodUsernamePassword) 39 | tests.AssertErrorContains(t, err, "invalid username/password version") 40 | 41 | buf = bytes.NewBuffer([]byte{byte(0x01), byte(0x02)}) 42 | err = auth.Authenticate(context.Background(), buf, AuthMethodUsernamePassword) 43 | tests.AssertErrorContains(t, err, "authentication failed") 44 | 45 | err = auth.Authenticate(context.Background(), buf, AuthMethodNoAcceptableMethods) 46 | tests.AssertErrorContains(t, err, "unsupported authentication method") 47 | 48 | } 49 | -------------------------------------------------------------------------------- /internal/testcert/testcert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package testcert contains a test-only localhost certificate. 6 | package testcert 7 | 8 | import "strings" 9 | 10 | // LocalhostCert is a PEM-encoded TLS cert with SAN IPs 11 | // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. 12 | // generated from src/crypto/tls: 13 | // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h 14 | var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- 15 | MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS 16 | MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw 17 | MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB 18 | iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 19 | iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul 20 | rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO 21 | BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw 22 | AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA 23 | AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 24 | tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs 25 | h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM 26 | fblo6RBxUQ== 27 | -----END CERTIFICATE-----`) 28 | 29 | // LocalhostKey is the private key for LocalhostCert. 30 | var LocalhostKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- 31 | MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9 32 | SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB 33 | l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB 34 | AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet 35 | 3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb 36 | uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H 37 | qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp 38 | jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY 39 | fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U 40 | fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU 41 | y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX 42 | qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo 43 | f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== 44 | -----END RSA TESTING KEY-----`)) 45 | 46 | func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } 47 | -------------------------------------------------------------------------------- /internal/testdata/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICzDCCAbQCCQDA+rLymNnfJzANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQKDB1x 3 | dWljLWdvIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0yMDA4MTgwOTIxMzVaFw0z 4 | MDA4MTYwOTIxMzVaMCgxJjAkBgNVBAoMHXF1aWMtZ28gQ2VydGlmaWNhdGUgQXV0 5 | aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1OcsYrVaSDfh 6 | iDppl6oteVspOY3yFb96T9Y/biaGPJAkBO9VGKcqwOUPmUeiWpedRAUB9LE7Srs6 7 | qBX4mnl90Icjp8jbIs5cPgIWLkIu8Qm549RghFzB3bn+EmCQSe4cxvyDMN3ndClp 8 | 3YMXpZgXWgJGiPOylVi/OwHDdWDBorw4hvry+6yDtpQo2TuI2A/xtxXPT7BgsEJD 9 | WGffdgZOYXChcFA0c1XVLIYlu2w2JhxS8c2TUF6uSDlmcoONNKVoiNCuu1Z9MorS 10 | Qmg7a2G7dSPu123KcTcSQFcmJrt+1G81gOBtHB69kacD8xDmgksj09h/ODPL/gIU 11 | 1ZcU2ci1/QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB0Tb1JbLXp/BvWovSAhO/j 12 | wG7UEaUA1rCtkDB+fV2HS9bxCbV5eErdg8AMHKgB51ygUrq95vm/baZmUILr84XK 13 | uTEoxxrw5S9Z7SrhtbOpKCumoSeTsCPjDvCcwFExHv4XHFk+CPqZwbMHueVIMT0+ 14 | nGWss/KecCPdJLdnUgMRz0tIuXzkoRuOiUiZfUeyBNVNbDFSrLigYshTeAPGaYjX 15 | CypoHxkeS93nWfOMUu8FTYLYkvGMU5i076zDoFGKJiEtbjSiNW+Hei7u2aSEuCzp 16 | qyTKzYPWYffAq3MM2MKJgZdL04e9GEGeuce/qhM1o3q77aI/XJImwEDdut2LDec1 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /internal/testdata/cert.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "path" 7 | "runtime" 8 | ) 9 | 10 | var certPath string 11 | 12 | func init() { 13 | _, filename, _, ok := runtime.Caller(0) 14 | if !ok { 15 | panic("Failed to get current frame") 16 | } 17 | 18 | certPath = path.Dir(filename) 19 | } 20 | 21 | // GetCertificatePaths returns the paths to certificate and key 22 | func GetCertificatePaths() (string, string) { 23 | return path.Join(certPath, "cert.pem"), path.Join(certPath, "priv.key") 24 | } 25 | 26 | // GetTLSConfig returns a tls config for quic.clemente.io 27 | func GetTLSConfig() *tls.Config { 28 | cert, err := tls.LoadX509KeyPair(GetCertificatePaths()) 29 | if err != nil { 30 | panic(err) 31 | } 32 | return &tls.Config{ 33 | Certificates: []tls.Certificate{cert}, 34 | } 35 | } 36 | 37 | // AddRootCA adds the root CA certificate to a cert pool 38 | func AddRootCA(certPool *x509.CertPool) { 39 | caCertPath := path.Join(certPath, "ca.pem") 40 | caCertRaw, err := os.ReadFile(caCertPath) 41 | if err != nil { 42 | panic(err) 43 | } 44 | if ok := certPool.AppendCertsFromPEM(caCertRaw); !ok { 45 | panic("Could not add root ceritificate to pool.") 46 | } 47 | } 48 | 49 | // GetRootCA returns an x509.CertPool containing (only) the CA certificate 50 | func GetRootCA() *x509.CertPool { 51 | pool := x509.NewCertPool() 52 | AddRootCA(pool) 53 | return pool 54 | } 55 | -------------------------------------------------------------------------------- /internal/testdata/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC1TCCAb2gAwIBAgIJAK2fcqC0BVA7MA0GCSqGSIb3DQEBCwUAMCgxJjAkBgNV 3 | BAoMHXF1aWMtZ28gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTIwMDgxODA5MjEz 4 | NVoXDTMwMDgxNjA5MjEzNVowEjEQMA4GA1UECgwHcXVpYy1nbzCCASIwDQYJKoZI 5 | hvcNAQEBBQADggEPADCCAQoCggEBAN/YwrigSXdJCL/bdBGhb0UpqtU8H+krV870 6 | +w1yCSykLImH8x3qHZEXt9sr/vgjcJoV6Z15RZmnbEqnAx84sIClIBoIgnk0VPxu 7 | WF+/U/dElbftCfYcfJAddhRckdmGB+yb3Wogb32UJ+q3my++h6NjHsYb+OwpJPnQ 8 | meXjOE7Kkf+bXfFywHF3R8kzVdh5JUFYeKbxYmYgxRps1YTsbCrZCrSy1CbQ9FJw 9 | Wg5C8t+7yvVFmOeWPECypBCz2xS2mu+kycMNIjIWMl0SL7oVM5cBkRKPeVIG/KcM 10 | i5+/4lRSLoPh0Txh2TKBWfpzLbIOdPU8/O7cAukIGWx0XsfHUQMCAwEAAaMYMBYw 11 | FAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQAyxxvebdMz 12 | shp5pt1SxMOSXbo8sTa1cpaf2rTmb4nxjXs6KPBEn53hSBz9bhe5wXE4f94SHadf 13 | 636rLh3d75KgrLUwO9Yq0HfCxMo1jUV/Ug++XwcHCI9vk58Tk/H4hqEM6C8RrdTj 14 | fYeuegQ0/oNLJ4uTw2P2A8TJbL6FC2dcICEAvUGZUcVyZ8m8tHXNRYYh6MZ7ubCh 15 | hinvL+AA5fY6EVlc5G/P4DN6fYxGn1cFNbiL4uZP4+W3dOmP+NV0YV9ihTyMzz0R 16 | vSoOZ9FeVkyw8EhMb3LoyXYKazvJy2VQST1ltzAGit9RiM1Gv4vuna74WsFzrn1U 17 | A/TbaR0ih/qG 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /internal/testdata/cert_test.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "crypto/tls" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("certificates", func() { 10 | It("returns certificates", func() { 11 | ln, err := tls.Listen("tcp", "localhost:4433", GetTLSConfig()) 12 | Expect(err).ToNot(HaveOccurred()) 13 | 14 | go func() { 15 | defer GinkgoRecover() 16 | conn, err := ln.Accept() 17 | Expect(err).ToNot(HaveOccurred()) 18 | defer conn.Close() 19 | _, err = conn.Write([]byte("foobar")) 20 | Expect(err).ToNot(HaveOccurred()) 21 | }() 22 | 23 | conn, err := tls.Dial("tcp", "localhost:4433", &tls.Config{RootCAs: GetRootCA()}) 24 | Expect(err).ToNot(HaveOccurred()) 25 | data, err := io.ReadAll(conn) 26 | Expect(err).ToNot(HaveOccurred()) 27 | Expect(string(data)).To(Equal("foobar")) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /internal/testdata/generate_key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Generating CA key and certificate:" 6 | openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:2048 \ 7 | -keyout ca.key -out ca.pem \ 8 | -subj "/O=quic-go Certificate Authority/" 9 | 10 | echo "Generating CSR" 11 | openssl req -out cert.csr -new -newkey rsa:2048 -nodes -keyout priv.key \ 12 | -subj "/O=quic-go/" 13 | 14 | echo "Sign certificate:" 15 | openssl x509 -req -sha256 -days 3650 -in cert.csr -out cert.pem \ 16 | -CA ca.pem -CAkey ca.key -CAcreateserial \ 17 | -extfile <(printf "subjectAltName=DNS:localhost") 18 | 19 | # debug output the certificate 20 | openssl x509 -noout -text -in cert.pem 21 | 22 | # we don't need the CA key, the serial number and the CSR any more 23 | rm ca.key cert.csr ca.srl 24 | 25 | -------------------------------------------------------------------------------- /internal/testdata/priv.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDf2MK4oEl3SQi/ 3 | 23QRoW9FKarVPB/pK1fO9PsNcgkspCyJh/Md6h2RF7fbK/74I3CaFemdeUWZp2xK 4 | pwMfOLCApSAaCIJ5NFT8blhfv1P3RJW37Qn2HHyQHXYUXJHZhgfsm91qIG99lCfq 5 | t5svvoejYx7GG/jsKST50Jnl4zhOypH/m13xcsBxd0fJM1XYeSVBWHim8WJmIMUa 6 | bNWE7Gwq2Qq0stQm0PRScFoOQvLfu8r1RZjnljxAsqQQs9sUtprvpMnDDSIyFjJd 7 | Ei+6FTOXAZESj3lSBvynDIufv+JUUi6D4dE8YdkygVn6cy2yDnT1PPzu3ALpCBls 8 | dF7Hx1EDAgMBAAECggEBAMm+mLDBdbUWk9YmuZNyRdC13wvT5obF05vo26OglXgw 9 | dxt09b6OVBuCnuff3SpS9pdJDIYq2HnFlSorH/sxopIvQKF17fHDIp1n7ipNTCXd 10 | IHrmHkY8Il/YzaVIUQMVc2rih0mw9greTqOS20DKnYC6QvAWIeDmrDaitTGl+ge3 11 | hm7e2lsgZi13R6fTNwQs9geEQSGzP2k7bFceHQFDChOYiQraR5+VZZ8S8AMGjk47 12 | AUa5EsKeUe6O9t2xuDSFxzYz5eadOAiErKGDos5KXXr3VQgFcC8uPEFFjcJ/yl+8 13 | tOe4iLeVwGSDJhTAThdR2deJOjaDcarWM7ixmxA3DAECgYEA/WVwmY4gWKwv49IJ 14 | Jnh1Gu93P772GqliMNpukdjTI+joQxfl4jRSt2hk4b1KRwyT9aaKfvdz0HFlXo/r 15 | 9NVSAYT3/3vbcw61bfvPhhtz44qRAAKua6b5cUM6XqxVt1hqdP8lrf/blvA5ln+u 16 | O51S8+wpxZMuqKz/29zdWSG6tAMCgYEA4iWXMXX9dZajI6abVkWwuosvOakXdLk4 17 | tUy7zd+JPF7hmUzzj2gtg4hXoiQPAOi+GY3TX+1Nza3s1LD7iWaXSKeOWvvligw9 18 | Q/wVTNW2P1+tdhScJf9QudzW69xOm5HNBgx9uWV2cHfjC12vg5aTH0k5axvaq15H 19 | 9WBXlH5q3wECgYBYoYGYBDFmMpvxmMagkSOMz1OrlVSpkLOKmOxx0SBRACc1SIec 20 | 7mY8RqR6nOX9IfYixyTMMittLiyhvb9vfKnZZDQGRcFFZlCpbplws+t+HDqJgWaW 21 | uumm5zfkY2z7204pLBF24fZhvha2gGRl76pTLTiTJd79Gr3HnmJByd1vFwKBgHL7 22 | vfYuEeM55lT4Hz8sTAFtR2O/7+cvTgAQteSlZbfGXlp939DonUulhTkxsFc7/3wq 23 | unCpzcdoSWSTYDGqcf1FBIKKVVltg7EPeR0KBJIQabgCHqrLOBZojPZ7m5RJ+765 24 | lysuxZvFuTFMPzNe2gssRf+JuBMt6tR+WclsxZYBAoGAEEFs1ppDil1xlP5rdH7T 25 | d3TSw/u4eU/X8Ei1zi25hdRUiV76fP9fBELYFmSrPBhugYv91vtSv/LmD4zLfLv/ 26 | yzwAD9j1lGbgM8Of8klCkk+XSJ88ryUwnMTJ5loQJW8t4L+zLv5Le7Ca9SAT0kJ1 27 | jT0GzDymgLMGp8RPdBkpk+w= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /internal/testdata/testdata_suite_test.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestTestdata(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Testdata Suite") 13 | } 14 | -------------------------------------------------------------------------------- /internal/tests/assert.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "go/token" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "unsafe" 9 | ) 10 | 11 | func AssertIsNil(t *testing.T, v interface{}) { 12 | if !isNil(v) { 13 | t.Errorf("[%v] was expected to be nil", v) 14 | } 15 | } 16 | 17 | func AssertAllNotNil(t *testing.T, vv ...interface{}) { 18 | for _, v := range vv { 19 | AssertNotNil(t, v) 20 | } 21 | } 22 | 23 | func AssertNotNil(t *testing.T, v interface{}) { 24 | if isNil(v) { 25 | t.Fatalf("[%v] was expected to be non-nil", v) 26 | } 27 | } 28 | 29 | func AssertEqual(t *testing.T, e, g interface{}) { 30 | if !equal(e, g) { 31 | t.Errorf("Expected [%+v], got [%+v]", e, g) 32 | } 33 | return 34 | } 35 | 36 | func AssertNoError(t *testing.T, err error) { 37 | if err != nil { 38 | t.Errorf("Error occurred [%v]", err) 39 | } 40 | } 41 | 42 | func AssertErrorContains(t *testing.T, err error, s string) { 43 | if err == nil { 44 | t.Error("err is nil") 45 | return 46 | } 47 | if !strings.Contains(err.Error(), s) { 48 | t.Errorf("%q is not included in error %q", s, err.Error()) 49 | } 50 | } 51 | 52 | func AssertContains(t *testing.T, s, substr string, shouldContain bool) { 53 | s = strings.ToLower(s) 54 | isContain := strings.Contains(s, substr) 55 | if shouldContain { 56 | if !isContain { 57 | t.Errorf("%q is not included in %s", substr, s) 58 | } 59 | } else { 60 | if isContain { 61 | t.Errorf("%q is included in %q", substr, s) 62 | } 63 | } 64 | } 65 | 66 | func AssertClone(t *testing.T, e, g interface{}) { 67 | ev := reflect.ValueOf(e).Elem() 68 | gv := reflect.ValueOf(g).Elem() 69 | et := ev.Type() 70 | 71 | for i := 0; i < ev.NumField(); i++ { 72 | sf := ev.Field(i) 73 | st := et.Field(i) 74 | 75 | var ee, gg interface{} 76 | if !token.IsExported(st.Name) { 77 | ee = reflect.NewAt(sf.Type(), unsafe.Pointer(sf.UnsafeAddr())).Elem().Interface() 78 | gg = reflect.NewAt(sf.Type(), unsafe.Pointer(gv.Field(i).UnsafeAddr())).Elem().Interface() 79 | } else { 80 | ee = sf.Interface() 81 | gg = gv.Field(i).Interface() 82 | } 83 | if sf.Kind() == reflect.Func || sf.Kind() == reflect.Slice || sf.Kind() == reflect.Ptr { 84 | if ee != nil { 85 | if gg == nil { 86 | t.Errorf("Field %s.%s is nil", et.Name(), et.Field(i).Name) 87 | } 88 | } 89 | continue 90 | } 91 | if !reflect.DeepEqual(ee, gg) { 92 | t.Errorf("Field %s.%s is not equal, expected [%v], got [%v]", et.Name(), et.Field(i).Name, ee, gg) 93 | } 94 | } 95 | } 96 | 97 | func equal(expected, got interface{}) bool { 98 | return reflect.DeepEqual(expected, got) 99 | } 100 | 101 | func isNil(v interface{}) bool { 102 | if v == nil { 103 | return true 104 | } 105 | rv := reflect.ValueOf(v) 106 | kind := rv.Kind() 107 | if kind >= reflect.Chan && kind <= reflect.Slice && rv.IsNil() { 108 | return true 109 | } 110 | return false 111 | } 112 | -------------------------------------------------------------------------------- /internal/tests/condition.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "time" 4 | 5 | // WaitCondition reports whether fn eventually returned true, 6 | // checking immediately and then every checkEvery amount, 7 | // until waitFor has elapsed, at which point it returns false. 8 | func WaitCondition(waitFor, checkEvery time.Duration, fn func() bool) bool { 9 | deadline := time.Now().Add(waitFor) 10 | for time.Now().Before(deadline) { 11 | if fn() { 12 | return true 13 | } 14 | time.Sleep(checkEvery) 15 | } 16 | return false 17 | } 18 | -------------------------------------------------------------------------------- /internal/tests/file.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var testDataPath string 9 | 10 | func init() { 11 | pwd, _ := os.Getwd() 12 | testDataPath = filepath.Join(pwd, ".testdata") 13 | } 14 | 15 | // GetTestFilePath return test file absolute path. 16 | func GetTestFilePath(filename string) string { 17 | return filepath.Join(testDataPath, filename) 18 | } 19 | -------------------------------------------------------------------------------- /internal/tests/net.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func NewLocalListener(t *testing.T) net.Listener { 9 | ln, err := net.Listen("tcp", "127.0.0.1:0") 10 | if err != nil { 11 | ln, err = net.Listen("tcp6", "[::1]:0") 12 | } 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | return ln 17 | } 18 | -------------------------------------------------------------------------------- /internal/tests/reader.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | type NeverEnding byte 4 | 5 | func (b NeverEnding) Read(p []byte) (int, error) { 6 | for i := range p { 7 | p[i] = byte(b) 8 | } 9 | return len(p), nil 10 | } 11 | -------------------------------------------------------------------------------- /internal/tests/transport.go: -------------------------------------------------------------------------------- 1 | package tests 2 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "os" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | // IsJSONType method is to check JSON content type or not 12 | func IsJSONType(ct string) bool { 13 | return strings.Contains(ct, "json") 14 | } 15 | 16 | // IsXMLType method is to check XML content type or not 17 | func IsXMLType(ct string) bool { 18 | return strings.Contains(ct, "xml") 19 | } 20 | 21 | // GetPointer return the pointer of the interface. 22 | func GetPointer(v interface{}) interface{} { 23 | t := reflect.TypeOf(v) 24 | if t.Kind() == reflect.Ptr { 25 | if tt := t.Elem(); tt.Kind() == reflect.Ptr { // pointer of pointer 26 | if tt.Elem().Kind() == reflect.Ptr { 27 | panic("pointer of pointer of pointer is not supported") 28 | } 29 | el := reflect.ValueOf(v).Elem() 30 | if el.IsZero() { 31 | vv := reflect.New(tt.Elem()) 32 | el.Set(vv) 33 | return vv.Interface() 34 | } else { 35 | return el.Interface() 36 | } 37 | } else { 38 | if reflect.ValueOf(v).IsZero() { 39 | vv := reflect.New(t.Elem()) 40 | return vv.Interface() 41 | } 42 | return v 43 | } 44 | } 45 | return reflect.New(t).Interface() 46 | } 47 | 48 | // GetType return the underlying type. 49 | func GetType(v interface{}) reflect.Type { 50 | return reflect.Indirect(reflect.ValueOf(v)).Type() 51 | } 52 | 53 | // CutString slices s around the first instance of sep, 54 | // returning the text before and after sep. 55 | // The found result reports whether sep appears in s. 56 | // If sep does not appear in s, cut returns s, "", false. 57 | func CutString(s, sep string) (before, after string, found bool) { 58 | if i := strings.Index(s, sep); i >= 0 { 59 | return s[:i], s[i+len(sep):], true 60 | } 61 | return s, "", false 62 | } 63 | 64 | // CutBytes slices s around the first instance of sep, 65 | // returning the text before and after sep. 66 | // The found result reports whether sep appears in s. 67 | // If sep does not appear in s, cut returns s, nil, false. 68 | // 69 | // CutBytes returns slices of the original slice s, not copies. 70 | func CutBytes(s, sep []byte) (before, after []byte, found bool) { 71 | if i := bytes.Index(s, sep); i >= 0 { 72 | return s[:i], s[i+len(sep):], true 73 | } 74 | return s, nil, false 75 | } 76 | 77 | // IsStringEmpty method tells whether given string is empty or not 78 | func IsStringEmpty(str string) bool { 79 | return len(strings.TrimSpace(str)) == 0 80 | } 81 | 82 | // See 2 (end of page 4) https://www.ietf.org/rfc/rfc2617.txt 83 | // "To receive authorization, the client sends the userid and password, 84 | // separated by a single colon (":") character, within a base64 85 | // encoded string in the credentials." 86 | // It is not meant to be urlencoded. 87 | func basicAuth(username, password string) string { 88 | auth := username + ":" + password 89 | return base64.StdEncoding.EncodeToString([]byte(auth)) 90 | } 91 | 92 | // BasicAuthHeaderValue return the header of basic auth. 93 | func BasicAuthHeaderValue(username, password string) string { 94 | return "Basic " + basicAuth(username, password) 95 | } 96 | 97 | // CreateDirectory create the directory. 98 | func CreateDirectory(dir string) (err error) { 99 | if _, err = os.Stat(dir); err != nil { 100 | if os.IsNotExist(err) { 101 | if err = os.MkdirAll(dir, 0755); err != nil { 102 | return 103 | } 104 | } 105 | } 106 | return 107 | } 108 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | // Logger is the abstract logging interface, gives control to 10 | // the Req users, choice of the logger. 11 | type Logger interface { 12 | Errorf(format string, v ...interface{}) 13 | Warnf(format string, v ...interface{}) 14 | Debugf(format string, v ...interface{}) 15 | } 16 | 17 | // NewLogger create a Logger wraps the *log.Logger 18 | func NewLogger(output io.Writer, prefix string, flag int) Logger { 19 | return &logger{l: log.New(output, prefix, flag)} 20 | } 21 | 22 | func NewLoggerFromStandardLogger(l *log.Logger) Logger { 23 | return &logger{l: l} 24 | } 25 | 26 | func createDefaultLogger() Logger { 27 | return NewLogger(os.Stdout, "", log.Ldate|log.Lmicroseconds) 28 | } 29 | 30 | var _ Logger = (*logger)(nil) 31 | 32 | type disableLogger struct{} 33 | 34 | func (l *disableLogger) Errorf(format string, v ...interface{}) {} 35 | func (l *disableLogger) Warnf(format string, v ...interface{}) {} 36 | func (l *disableLogger) Debugf(format string, v ...interface{}) {} 37 | 38 | type logger struct { 39 | l *log.Logger 40 | } 41 | 42 | func (l *logger) Errorf(format string, v ...interface{}) { 43 | l.output("ERROR", format, v...) 44 | } 45 | 46 | func (l *logger) Warnf(format string, v ...interface{}) { 47 | l.output("WARN", format, v...) 48 | } 49 | 50 | func (l *logger) Debugf(format string, v ...interface{}) { 51 | l.output("DEBUG", format, v...) 52 | } 53 | 54 | func (l *logger) output(level, format string, v ...interface{}) { 55 | format = level + " [req] " + format 56 | if len(v) == 0 { 57 | l.l.Print(format) 58 | return 59 | } 60 | l.l.Printf(format, v...) 61 | } 62 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | 8 | "github.com/imroc/req/v3/internal/tests" 9 | ) 10 | 11 | func TestLogger(t *testing.T) { 12 | buf := new(bytes.Buffer) 13 | l := NewLogger(buf, "", log.Ldate|log.Lmicroseconds) 14 | c := tc().SetLogger(l) 15 | c.SetProxyURL(":=\\<>ksfj&*&sf") 16 | tests.AssertContains(t, buf.String(), "error", true) 17 | buf.Reset() 18 | c.R().SetOutput(nil) 19 | tests.AssertContains(t, buf.String(), "warn", true) 20 | } 21 | 22 | func TestLoggerFromStandardLogger(t *testing.T) { 23 | buf := new(bytes.Buffer) 24 | l := NewLoggerFromStandardLogger(log.New(buf, "", log.Ldate|log.Lmicroseconds)) 25 | c := tc().SetLogger(l) 26 | c.SetProxyURL(":=\\<>ksfj&*&sf") 27 | tests.AssertContains(t, buf.String(), "error", true) 28 | buf.Reset() 29 | c.R().SetOutput(nil) 30 | tests.AssertContains(t, buf.String(), "warn", true) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/altsvc/altsvc.go: -------------------------------------------------------------------------------- 1 | package altsvc 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // AltSvcJar is default implementation of Jar, which stores 9 | // AltSvc in memory. 10 | type AltSvcJar struct { 11 | entries map[string]*AltSvc 12 | mu sync.Mutex 13 | } 14 | 15 | // NewAltSvcJar create a AltSvcJar which implements Jar. 16 | func NewAltSvcJar() *AltSvcJar { 17 | return &AltSvcJar{ 18 | entries: make(map[string]*AltSvc), 19 | } 20 | } 21 | 22 | func (j *AltSvcJar) GetAltSvc(addr string) *AltSvc { 23 | if addr == "" { 24 | return nil 25 | } 26 | as, ok := j.entries[addr] 27 | if !ok { 28 | return nil 29 | } 30 | now := time.Now() 31 | j.mu.Lock() 32 | defer j.mu.Unlock() 33 | if as.Expire.Before(now) { // expired 34 | delete(j.entries, addr) 35 | return nil 36 | } 37 | return as 38 | } 39 | 40 | func (j *AltSvcJar) SetAltSvc(addr string, as *AltSvc) { 41 | if addr == "" { 42 | return 43 | } 44 | j.mu.Lock() 45 | defer j.mu.Unlock() 46 | j.entries[addr] = as 47 | } 48 | 49 | // AltSvc is the parsed alt-svc. 50 | type AltSvc struct { 51 | // Protocol is the alt-svc proto, e.g. h3. 52 | Protocol string 53 | // Host is the alt-svc's host, could be empty if 54 | // it's the same host as the raw request. 55 | Host string 56 | // Port is the alt-svc's port. 57 | Port string 58 | // Expire is the time that the alt-svc should expire. 59 | Expire time.Time 60 | } 61 | -------------------------------------------------------------------------------- /pkg/altsvc/jar.go: -------------------------------------------------------------------------------- 1 | package altsvc 2 | 3 | // Jar is a container of AltSvc. 4 | type Jar interface { 5 | // SetAltSvc store the AltSvc. 6 | SetAltSvc(addr string, as *AltSvc) 7 | // GetAltSvc get the AltSvc. 8 | GetAltSvc(addr string) *AltSvc 9 | } 10 | -------------------------------------------------------------------------------- /pkg/tls/conn.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net" 7 | ) 8 | 9 | // Conn is the recommended interface for the connection 10 | // returned by the DailTLS function (Client.SetDialTLS, 11 | // Transport.DialTLSContext), so that the TLS handshake negotiation 12 | // can automatically decide whether to use HTTP2 or HTTP1 (ALPN). 13 | // If this interface is not implemented, HTTP1 will be used by default. 14 | type Conn interface { 15 | net.Conn 16 | // ConnectionState returns basic TLS details about the connection. 17 | ConnectionState() tls.ConnectionState 18 | // Handshake runs the client or server handshake 19 | // protocol if it has not yet been run. 20 | // 21 | // Most uses of this package need not call Handshake explicitly: the 22 | // first Read or Write will call it automatically. 23 | // 24 | // For control over canceling or setting a timeout on a handshake, use 25 | // HandshakeContext or the Dialer's DialContext method instead. 26 | Handshake() error 27 | 28 | // HandshakeContext runs the client or server handshake 29 | // protocol if it has not yet been run. 30 | // 31 | // The provided Context must be non-nil. If the context is canceled before 32 | // the handshake is complete, the handshake is interrupted and an error is returned. 33 | // Once the handshake has completed, cancellation of the context will not affect the 34 | // connection. 35 | // 36 | // Most uses of this package need not call HandshakeContext explicitly: the 37 | // first Read or Write will call it automatically. 38 | HandshakeContext(ctx context.Context) error 39 | } 40 | -------------------------------------------------------------------------------- /redirect.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | // RedirectPolicy represents the redirect policy for Client. 12 | type RedirectPolicy func(req *http.Request, via []*http.Request) error 13 | 14 | // MaxRedirectPolicy specifies the max number of redirect 15 | func MaxRedirectPolicy(noOfRedirect int) RedirectPolicy { 16 | return func(req *http.Request, via []*http.Request) error { 17 | if len(via) >= noOfRedirect { 18 | return fmt.Errorf("stopped after %d redirects", noOfRedirect) 19 | } 20 | return nil 21 | } 22 | } 23 | 24 | // DefaultRedirectPolicy allows up to 10 redirects 25 | func DefaultRedirectPolicy() RedirectPolicy { 26 | return MaxRedirectPolicy(10) 27 | } 28 | 29 | // NoRedirectPolicy disable redirect behaviour 30 | func NoRedirectPolicy() RedirectPolicy { 31 | return func(req *http.Request, via []*http.Request) error { 32 | return http.ErrUseLastResponse 33 | } 34 | } 35 | 36 | // SameDomainRedirectPolicy allows redirect only if the redirected domain 37 | // is the same as original domain, e.g. redirect to "www.imroc.cc" from 38 | // "imroc.cc" is allowed, but redirect to "google.com" is not allowed. 39 | func SameDomainRedirectPolicy() RedirectPolicy { 40 | return func(req *http.Request, via []*http.Request) error { 41 | if getDomain(req.URL.Host) != getDomain(via[0].URL.Host) { 42 | return errors.New("different domain name is not allowed") 43 | } 44 | return nil 45 | } 46 | } 47 | 48 | // SameHostRedirectPolicy allows redirect only if the redirected host 49 | // is the same as original host, e.g. redirect to "www.imroc.cc" from 50 | // "imroc.cc" is not the allowed. 51 | func SameHostRedirectPolicy() RedirectPolicy { 52 | return func(req *http.Request, via []*http.Request) error { 53 | if getHostname(req.URL.Host) != getHostname(via[0].URL.Host) { 54 | return errors.New("different host name is not allowed") 55 | } 56 | return nil 57 | } 58 | } 59 | 60 | // AllowedHostRedirectPolicy allows redirect only if the redirected host 61 | // match one of the host that specified. 62 | func AllowedHostRedirectPolicy(hosts ...string) RedirectPolicy { 63 | m := make(map[string]struct{}) 64 | for _, h := range hosts { 65 | m[strings.ToLower(getHostname(h))] = struct{}{} 66 | } 67 | 68 | return func(req *http.Request, via []*http.Request) error { 69 | h := getHostname(req.URL.Host) 70 | if _, ok := m[h]; !ok { 71 | return fmt.Errorf("redirect host [%s] is not allowed", h) 72 | } 73 | return nil 74 | } 75 | } 76 | 77 | // AllowedDomainRedirectPolicy allows redirect only if the redirected domain 78 | // match one of the domain that specified. 79 | func AllowedDomainRedirectPolicy(hosts ...string) RedirectPolicy { 80 | domains := make(map[string]struct{}) 81 | for _, h := range hosts { 82 | domains[strings.ToLower(getDomain(h))] = struct{}{} 83 | } 84 | 85 | return func(req *http.Request, via []*http.Request) error { 86 | domain := getDomain(req.URL.Host) 87 | if _, ok := domains[domain]; !ok { 88 | return fmt.Errorf("redirect domain [%s] is not allowed", domain) 89 | } 90 | return nil 91 | } 92 | } 93 | 94 | func getHostname(host string) (hostname string) { 95 | if strings.Index(host, ":") > 0 { 96 | host, _, _ = net.SplitHostPort(host) 97 | } 98 | hostname = strings.ToLower(host) 99 | return 100 | } 101 | 102 | func getDomain(host string) string { 103 | host = getHostname(host) 104 | ss := strings.Split(host, ".") 105 | if len(ss) < 3 { 106 | return host 107 | } 108 | ss = ss[1:] 109 | return strings.Join(ss, ".") 110 | } 111 | 112 | // AlwaysCopyHeaderRedirectPolicy ensures that the given sensitive headers will 113 | // always be copied on redirect. 114 | // By default, golang will copy all of the original request's headers on redirect, 115 | // unless they're sensitive, like "Authorization" or "Www-Authenticate". Only send 116 | // sensitive ones to the same origin, or subdomains thereof (https://go-review.googlesource.com/c/go/+/28930/) 117 | // Check discussion: https://github.com/golang/go/issues/4800 118 | // For example: 119 | // 120 | // client.SetRedirectPolicy(req.AlwaysCopyHeaderRedirectPolicy("Authorization")) 121 | func AlwaysCopyHeaderRedirectPolicy(headers ...string) RedirectPolicy { 122 | return func(req *http.Request, via []*http.Request) error { 123 | for _, header := range headers { 124 | if len(req.Header.Values(header)) > 0 { 125 | continue 126 | } 127 | vals := via[0].Header.Values(header) 128 | for _, val := range vals { 129 | req.Header.Add(header, val) 130 | } 131 | } 132 | return nil 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | type kv struct { 11 | Key string 12 | Value string 13 | } 14 | 15 | // ContentDisposition represents parameters in `Content-Disposition` 16 | // MIME header of multipart request. 17 | type ContentDisposition struct { 18 | kv []kv 19 | } 20 | 21 | // Add adds a new key-value pair of Content-Disposition 22 | func (c *ContentDisposition) Add(key, value string) *ContentDisposition { 23 | c.kv = append(c.kv, kv{Key: key, Value: value}) 24 | return c 25 | } 26 | 27 | func (c *ContentDisposition) string() string { 28 | if c == nil { 29 | return "" 30 | } 31 | s := "" 32 | for _, kv := range c.kv { 33 | s += fmt.Sprintf("; %s=%q", kv.Key, kv.Value) 34 | } 35 | return s 36 | } 37 | 38 | // FileUpload represents a "form-data" multipart 39 | type FileUpload struct { 40 | // "name" parameter in `Content-Disposition` 41 | ParamName string 42 | // "filename" parameter in `Content-Disposition` 43 | FileName string 44 | // The file to be uploaded. 45 | GetFileContent GetContentFunc 46 | // Optional file length in bytes. 47 | FileSize int64 48 | // Optional Content-Type 49 | ContentType string 50 | 51 | // Optional extra ContentDisposition parameters. 52 | // According to the HTTP specification, this should be nil, 53 | // but some servers may not follow the specification and 54 | // requires `Content-Disposition` parameters more than just 55 | // "name" and "filename". 56 | ExtraContentDisposition *ContentDisposition 57 | } 58 | 59 | // UploadInfo is the information for each UploadCallback call. 60 | type UploadInfo struct { 61 | // parameter name in multipart upload 62 | ParamName string 63 | // filename in multipart upload 64 | FileName string 65 | // total file length in bytes. 66 | FileSize int64 67 | // uploaded file length in bytes. 68 | UploadedSize int64 69 | } 70 | 71 | // UploadCallback is the callback which will be invoked during 72 | // multipart upload. 73 | type UploadCallback func(info UploadInfo) 74 | 75 | // DownloadInfo is the information for each DownloadCallback call. 76 | type DownloadInfo struct { 77 | // Response is the corresponding Response during download. 78 | Response *Response 79 | // downloaded body length in bytes. 80 | DownloadedSize int64 81 | } 82 | 83 | // DownloadCallback is the callback which will be invoked during 84 | // response body download. 85 | type DownloadCallback func(info DownloadInfo) 86 | 87 | func cloneSlice[T any](s []T) []T { 88 | if len(s) == 0 { 89 | return nil 90 | } 91 | ss := make([]T, len(s)) 92 | copy(ss, s) 93 | return ss 94 | } 95 | 96 | func cloneUrlValues(v url.Values) url.Values { 97 | if v == nil { 98 | return nil 99 | } 100 | vv := make(url.Values) 101 | for key, values := range v { 102 | for _, value := range values { 103 | vv.Add(key, value) 104 | } 105 | } 106 | return vv 107 | } 108 | 109 | func cloneMap(h map[string]string) map[string]string { 110 | if h == nil { 111 | return nil 112 | } 113 | m := make(map[string]string) 114 | for k, v := range h { 115 | m[k] = v 116 | } 117 | return m 118 | } 119 | 120 | // convertHeaderToString converts http header to a string. 121 | func convertHeaderToString(h http.Header) string { 122 | if h == nil { 123 | return "" 124 | } 125 | buf := new(bytes.Buffer) 126 | h.Write(buf) 127 | return buf.String() 128 | } 129 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | func defaultGetRetryInterval(resp *Response, attempt int) time.Duration { 10 | return 100 * time.Millisecond 11 | } 12 | 13 | // RetryConditionFunc is a retry condition, which determines 14 | // whether the request should retry. 15 | type RetryConditionFunc func(resp *Response, err error) bool 16 | 17 | // RetryHookFunc is a retry hook which will be executed before a retry. 18 | type RetryHookFunc func(resp *Response, err error) 19 | 20 | // GetRetryIntervalFunc is a function that determines how long should 21 | // sleep between retry attempts. 22 | type GetRetryIntervalFunc func(resp *Response, attempt int) time.Duration 23 | 24 | func backoffInterval(min, max time.Duration) GetRetryIntervalFunc { 25 | base := float64(min) 26 | capLevel := float64(max) 27 | return func(resp *Response, attempt int) time.Duration { 28 | temp := math.Min(capLevel, base*math.Exp2(float64(attempt))) 29 | halfTemp := int64(temp / 2) 30 | sleep := halfTemp + rand.Int63n(halfTemp) 31 | return time.Duration(sleep) 32 | } 33 | } 34 | 35 | func newDefaultRetryOption() *retryOption { 36 | return &retryOption{ 37 | GetRetryInterval: defaultGetRetryInterval, 38 | } 39 | } 40 | 41 | type retryOption struct { 42 | MaxRetries int 43 | GetRetryInterval GetRetryIntervalFunc 44 | RetryConditions []RetryConditionFunc 45 | RetryHooks []RetryHookFunc 46 | } 47 | 48 | func (ro *retryOption) Clone() *retryOption { 49 | if ro == nil { 50 | return nil 51 | } 52 | o := &retryOption{ 53 | MaxRetries: ro.MaxRetries, 54 | GetRetryInterval: ro.GetRetryInterval, 55 | } 56 | o.RetryConditions = append(o.RetryConditions, ro.RetryConditions...) 57 | o.RetryHooks = append(o.RetryHooks, ro.RetryHooks...) 58 | return o 59 | } 60 | -------------------------------------------------------------------------------- /roundtrip.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !js 6 | 7 | package req 8 | 9 | import ( 10 | "net/http" 11 | ) 12 | 13 | // RoundTrip implements the RoundTripper interface. 14 | // 15 | // For higher-level HTTP client support (such as handling of cookies 16 | // and redirects), see Get, Post, and the Client type. 17 | // 18 | // Like the RoundTripper interface, the error types returned 19 | // by RoundTrip are unspecified. 20 | func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 21 | if t.wrappedRoundTrip != nil { 22 | resp, err = t.wrappedRoundTrip.RoundTrip(req) 23 | } else { 24 | resp, err = t.roundTrip(req) 25 | } 26 | if err != nil { 27 | return 28 | } 29 | if resp.ProtoMajor != 3 && t.altSvcJar != nil { 30 | if v := resp.Header.Get("alt-svc"); v != "" { 31 | t.handleAltSvc(req, v) 32 | } 33 | } 34 | t.handleResponseBody(resp, req) 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import "sync" 4 | 5 | const copyBufPoolSize = 32 * 1024 6 | 7 | var copyBufPool = sync.Pool{New: func() any { return new([copyBufPoolSize]byte) }} 8 | 9 | func getCopyBuf() []byte { 10 | return copyBufPool.Get().(*[copyBufPoolSize]byte)[:] 11 | } 12 | 13 | func putCopyBuf(b []byte) { 14 | if len(b) != copyBufPoolSize { 15 | panic("trying to put back buffer of the wrong size in the copyBufPool") 16 | } 17 | copyBufPool.Put((*[copyBufPoolSize]byte)(b)) 18 | } 19 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "net/http/httptrace" 9 | "time" 10 | ) 11 | 12 | const ( 13 | traceFmt = `TotalTime : %v 14 | DNSLookupTime : %v 15 | TCPConnectTime : %v 16 | TLSHandshakeTime : %v 17 | FirstResponseTime : %v 18 | ResponseTime : %v 19 | IsConnReused: : false 20 | RemoteAddr : %v 21 | LocalAddr : %v` 22 | traceReusedFmt = `TotalTime : %v 23 | FirstResponseTime : %v 24 | ResponseTime : %v 25 | IsConnReused: : true 26 | RemoteAddr : %v 27 | LocalAddr : %v` 28 | ) 29 | 30 | // Blame return the human-readable reason of why request is slowing. 31 | func (t TraceInfo) Blame() string { 32 | if t.RemoteAddr == nil { 33 | return "trace is not enabled" 34 | } 35 | var mk string 36 | var mv time.Duration 37 | m := map[string]time.Duration{ 38 | "on dns lookup": t.DNSLookupTime, 39 | "on tcp connect": t.TCPConnectTime, 40 | "on tls handshake": t.TLSHandshakeTime, 41 | "from connection ready to server respond first byte": t.FirstResponseTime, 42 | "from server respond first byte to request completion": t.ResponseTime, 43 | } 44 | for k, v := range m { 45 | if v > mv { 46 | mk = k 47 | mv = v 48 | } 49 | } 50 | if mk == "" { 51 | return "nothing to blame" 52 | } 53 | return fmt.Sprintf("the request total time is %v, and costs %v %s", t.TotalTime, mv, mk) 54 | } 55 | 56 | // String return the details of trace information. 57 | func (t TraceInfo) String() string { 58 | if t.RemoteAddr == nil { 59 | return "trace is not enabled" 60 | } 61 | if t.IsConnReused { 62 | return fmt.Sprintf(traceReusedFmt, t.TotalTime, t.FirstResponseTime, t.ResponseTime, t.RemoteAddr, t.LocalAddr) 63 | } 64 | return fmt.Sprintf(traceFmt, t.TotalTime, t.DNSLookupTime, t.TCPConnectTime, t.TLSHandshakeTime, t.FirstResponseTime, t.ResponseTime, t.RemoteAddr, t.LocalAddr) 65 | } 66 | 67 | // TraceInfo represents the trace information. 68 | type TraceInfo struct { 69 | // DNSLookupTime is a duration that transport took to perform 70 | // DNS lookup. 71 | DNSLookupTime time.Duration 72 | 73 | // ConnectTime is a duration that took to obtain a successful connection. 74 | ConnectTime time.Duration 75 | 76 | // TCPConnectTime is a duration that took to obtain the TCP connection. 77 | TCPConnectTime time.Duration 78 | 79 | // TLSHandshakeTime is a duration that TLS handshake took place. 80 | TLSHandshakeTime time.Duration 81 | 82 | // FirstResponseTime is a duration that server took to respond first byte since 83 | // connection ready (after tls handshake if it's tls and not a reused connection). 84 | FirstResponseTime time.Duration 85 | 86 | // ResponseTime is a duration since first response byte from server to 87 | // request completion. 88 | ResponseTime time.Duration 89 | 90 | // TotalTime is a duration that total request took end-to-end. 91 | TotalTime time.Duration 92 | 93 | // IsConnReused is whether this connection has been previously 94 | // used for another HTTP request. 95 | IsConnReused bool 96 | 97 | // IsConnWasIdle is whether this connection was obtained from an 98 | // idle pool. 99 | IsConnWasIdle bool 100 | 101 | // ConnIdleTime is a duration how long the connection was previously 102 | // idle, if IsConnWasIdle is true. 103 | ConnIdleTime time.Duration 104 | 105 | // RemoteAddr returns the remote network address. 106 | RemoteAddr net.Addr 107 | 108 | // LocalAddr returns the local network address. 109 | LocalAddr net.Addr 110 | } 111 | 112 | type clientTrace struct { 113 | getConn time.Time 114 | dnsStart time.Time 115 | dnsDone time.Time 116 | connectDone time.Time 117 | tlsHandshakeStart time.Time 118 | tlsHandshakeDone time.Time 119 | gotConn time.Time 120 | gotFirstResponseByte time.Time 121 | endTime time.Time 122 | gotConnInfo httptrace.GotConnInfo 123 | } 124 | 125 | func (t *clientTrace) createContext(ctx context.Context) context.Context { 126 | return httptrace.WithClientTrace( 127 | ctx, 128 | &httptrace.ClientTrace{ 129 | DNSStart: func(_ httptrace.DNSStartInfo) { 130 | t.dnsStart = time.Now() 131 | }, 132 | DNSDone: func(_ httptrace.DNSDoneInfo) { 133 | t.dnsDone = time.Now() 134 | }, 135 | ConnectStart: func(_, _ string) { 136 | if t.dnsDone.IsZero() { 137 | t.dnsDone = time.Now() 138 | } 139 | if t.dnsStart.IsZero() { 140 | t.dnsStart = t.dnsDone 141 | } 142 | }, 143 | ConnectDone: func(net, addr string, err error) { 144 | t.connectDone = time.Now() 145 | }, 146 | GetConn: func(_ string) { 147 | t.getConn = time.Now() 148 | }, 149 | GotConn: func(ci httptrace.GotConnInfo) { 150 | t.gotConn = time.Now() 151 | t.gotConnInfo = ci 152 | }, 153 | GotFirstResponseByte: func() { 154 | t.gotFirstResponseByte = time.Now() 155 | }, 156 | TLSHandshakeStart: func() { 157 | t.tlsHandshakeStart = time.Now() 158 | }, 159 | TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { 160 | t.tlsHandshakeDone = time.Now() 161 | }, 162 | }, 163 | ) 164 | } 165 | -------------------------------------------------------------------------------- /transport_default_other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !js || !wasm 6 | 7 | package req 8 | 9 | import ( 10 | "context" 11 | "net" 12 | ) 13 | 14 | func defaultTransportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) { 15 | return dialer.DialContext 16 | } 17 | -------------------------------------------------------------------------------- /transport_default_wasm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build (js && wasm) || wasip1 6 | 7 | package req 8 | 9 | import ( 10 | "context" 11 | "net" 12 | ) 13 | 14 | func defaultTransportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) { 15 | return nil 16 | } 17 | --------------------------------------------------------------------------------