From 2537c69b0f9dd26abdcefefa1ca3835b90c94dec Mon Sep 17 00:00:00 2001
From: sago35
Date: Wed, 14 Jul 2021 22:46:24 +0900
Subject: [PATCH] rtl8720dn: add support for http.Get(), http.Post() and cookie
---
examples/rtl8720dn/tlsclient/main.go | 94 ++--
examples/rtl8720dn/webclient-tinyterm/main.go | 102 ++--
examples/rtl8720dn/webclient/main.go | 104 ++--
examples/rtl8720dn/webserver/main.go | 32 +-
net/http/client.go | 214 ++++++++
net/http/cookie.go | 433 +++++++++++++++
net/http/cookiejar/jar.go | 504 ++++++++++++++++++
net/http/cookiejar/punycode.go | 159 ++++++
net/http/jar.go | 27 +
net/http/request.go | 161 ++++++
net/http/response.go | 5 +
net/http/tinygo.go | 287 ++++++++++
net/ipsocki.go | 26 +
rtl8720dn/netdriver.go | 22 +-
rtl8720dn/rtl8720dn.go | 2 +
15 files changed, 1997 insertions(+), 175 deletions(-)
create mode 100644 net/http/client.go
create mode 100644 net/http/cookie.go
create mode 100644 net/http/cookiejar/jar.go
create mode 100644 net/http/cookiejar/punycode.go
create mode 100644 net/http/jar.go
create mode 100644 net/http/tinygo.go
create mode 100644 net/ipsocki.go
diff --git a/examples/rtl8720dn/tlsclient/main.go b/examples/rtl8720dn/tlsclient/main.go
index c6b2855..f0c83d9 100644
--- a/examples/rtl8720dn/tlsclient/main.go
+++ b/examples/rtl8720dn/tlsclient/main.go
@@ -1,12 +1,13 @@
package main
import (
+ "bufio"
"fmt"
"strings"
"time"
"tinygo.org/x/drivers/net"
- "tinygo.org/x/drivers/net/tls"
+ "tinygo.org/x/drivers/net/http"
"tinygo.org/x/drivers/rtl8720dn"
)
@@ -15,14 +16,14 @@ import (
// ssid = "your-ssid"
// password = "your-password"
// debug = true
-// server = "tinygo.org"
+// url = "https://www.example.com"
// test_root_ca = "..."
// }
var (
ssid string
password string
- server string = "www.example.com"
+ url string = "https://www.example.com"
debug = false
)
@@ -52,7 +53,7 @@ CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
-----END CERTIFICATE-----
`
-var buf [0x400]byte
+var buf [0x1000]byte
var lastRequestTime time.Time
var conn net.Conn
@@ -73,6 +74,7 @@ func run() error {
}
rtl.SetRootCA(&test_root_ca)
net.UseDriver(rtl)
+ http.SetBuf(buf[:])
err = rtl.ConnectToAP(ssid, password)
if err != nil {
@@ -87,55 +89,49 @@ func run() error {
fmt.Printf("Mask : %s\r\n", subnet)
fmt.Printf("Gateway : %s\r\n", gateway)
+ // You can send and receive cookies in the following way
+ // import "tinygo.org/x/drivers/net/http/cookiejar"
+ // jar, err := cookiejar.New(nil)
+ // if err != nil {
+ // return err
+ // }
+ // client := &http.Client{Jar: jar}
+ // http.DefaultClient = client
+
cnt := 0
for {
- readConnection()
- if time.Now().Sub(lastRequestTime).Milliseconds() >= 10000 {
- makeHTTPSRequest()
- cnt++
- fmt.Printf("-------- %d --------\r\n", cnt)
+ // Various examples are as follows
+ //
+ // -- Get
+ // resp, err := http.Get(url)
+ //
+ // -- Post
+ // body := `cnt=12`
+ // resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(body))
+ //
+ // -- Post with JSON
+ // body := `{"msg": "hello"}`
+ // resp, err := http.Post(url, "application/json", strings.NewReader(body))
+
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
}
- }
-}
-func readConnection() {
- if conn != nil {
- for n, err := conn.Read(buf[:]); n > 0; n, err = conn.Read(buf[:]) {
- if err != nil {
- println("Read error: " + err.Error())
- } else {
- print(string(buf[0:n]))
- }
+ fmt.Printf("%s %s\r\n", resp.Proto, resp.Status)
+ for k, v := range resp.Header {
+ fmt.Printf("%s: %s\r\n", k, strings.Join(v, " "))
}
+ fmt.Printf("\r\n")
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ fmt.Printf("%s\r\n", scanner.Text())
+ }
+ resp.Body.Close()
+
+ cnt++
+ fmt.Printf("-------- %d --------\r\n", cnt)
+ time.Sleep(10 * time.Second)
}
}
-
-func makeHTTPSRequest() {
-
- var err error
- if conn != nil {
- conn.Close()
- }
-
- message("\r\n---------------\r\nDialing TCP connection")
- conn, err = tls.Dial("tcp", server, nil)
- for ; err != nil; conn, err = tls.Dial("tcp", server, nil) {
- message("Connection failed: " + err.Error())
- time.Sleep(5 * time.Second)
- }
- println("Connected!\r")
-
- print("Sending HTTPS request...")
- fmt.Fprintln(conn, "GET / HTTP/1.1")
- fmt.Fprintln(conn, "Host:", strings.Split(server, ":")[0])
- fmt.Fprintln(conn, "User-Agent: TinyGo")
- fmt.Fprintln(conn, "Connection: close")
- fmt.Fprintln(conn)
- println("Sent!\r\n\r")
-
- lastRequestTime = time.Now()
-}
-
-func message(msg string) {
- println(msg, "\r")
-}
diff --git a/examples/rtl8720dn/webclient-tinyterm/main.go b/examples/rtl8720dn/webclient-tinyterm/main.go
index d2defef..ff8a1f1 100644
--- a/examples/rtl8720dn/webclient-tinyterm/main.go
+++ b/examples/rtl8720dn/webclient-tinyterm/main.go
@@ -1,12 +1,14 @@
package main
import (
+ "bufio"
"fmt"
"image/color"
+ "strings"
"time"
"tinygo.org/x/drivers/net"
- "tinygo.org/x/drivers/rtl8720dn"
+ "tinygo.org/x/drivers/net/http"
"tinygo.org/x/tinyfont/proggy"
"tinygo.org/x/tinyterm"
)
@@ -23,8 +25,8 @@ import (
var (
ssid string
password string
- server string = "tinygo.org"
- debug = false
+ url = "http://tinygo.org/"
+ debug = false
)
var (
@@ -41,10 +43,6 @@ var (
var buf [0x400]byte
-var lastRequestTime time.Time
-var conn net.Conn
-var adaptor *rtl8720dn.RTL8720DN
-
func main() {
display.FillScreen(black)
backlight.High()
@@ -73,6 +71,7 @@ func run() error {
return err
}
net.UseDriver(rtl)
+ http.SetBuf(buf[:])
fmt.Fprintf(terminal, "ConnectToAP()\r\n")
err = rtl.ConnectToAP(ssid, password)
@@ -89,60 +88,49 @@ func run() error {
fmt.Fprintf(terminal, "Mask : %s\r\n", subnet)
fmt.Fprintf(terminal, "Gateway : %s\r\n", gateway)
+ // You can send and receive cookies in the following way
+ // import "tinygo.org/x/drivers/net/http/cookiejar"
+ // jar, err := cookiejar.New(nil)
+ // if err != nil {
+ // return err
+ // }
+ // client := &http.Client{Jar: jar}
+ // http.DefaultClient = client
+
cnt := 0
for {
- readConnection()
- if time.Now().Sub(lastRequestTime).Milliseconds() >= 10000 {
- makeHTTPRequest()
- cnt++
- fmt.Fprintf(terminal, "-------- %d --------\r\n", cnt)
+ // Various examples are as follows
+ //
+ // -- Get
+ // resp, err := http.Get(url)
+ //
+ // -- Post
+ // body := `cnt=12`
+ // resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(body))
+ //
+ // -- Post with JSON
+ // body := `{"msg": "hello"}`
+ // resp, err := http.Post(url, "application/json", strings.NewReader(body))
+
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
}
- }
-}
-func readConnection() {
- if conn != nil {
- for n, err := conn.Read(buf[:]); n > 0; n, err = conn.Read(buf[:]) {
- if err != nil {
- fmt.Fprintf(terminal, "Read error: "+err.Error()+"\r\n")
- } else {
- fmt.Fprintf(terminal, string(buf[0:n]))
- }
+ fmt.Fprintf(terminal, "%s %s\r\n", resp.Proto, resp.Status)
+ for k, v := range resp.Header {
+ fmt.Fprintf(terminal, "%s: %s\r\n", k, strings.Join(v, " "))
}
+ fmt.Printf("\r\n")
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ fmt.Fprintf(terminal, "%s\r\n", scanner.Text())
+ }
+ resp.Body.Close()
+
+ cnt++
+ fmt.Fprintf(terminal, "-------- %d --------\r\n", cnt)
+ time.Sleep(10 * time.Second)
}
}
-
-func makeHTTPRequest() {
-
- var err error
- if conn != nil {
- conn.Close()
- }
-
- // make TCP connection
- ip := net.ParseIP(server)
- raddr := &net.TCPAddr{IP: ip, Port: 80}
- laddr := &net.TCPAddr{Port: 8080}
-
- message("\r\n---------------\r\nDialing TCP connection")
- conn, err = net.DialTCP("tcp", laddr, raddr)
- for ; err != nil; conn, err = net.DialTCP("tcp", laddr, raddr) {
- message("Connection failed: " + err.Error())
- time.Sleep(5 * time.Second)
- }
- fmt.Fprintf(terminal, "Connected!\r\n")
-
- fmt.Fprintf(terminal, "Sending HTTP request...")
- fmt.Fprintln(conn, "GET / HTTP/1.1")
- fmt.Fprintln(conn, "Host:", server)
- fmt.Fprintln(conn, "User-Agent: TinyGo")
- fmt.Fprintln(conn, "Connection: close")
- fmt.Fprintln(conn)
- fmt.Fprintf(terminal, "Sent!\r\n\r\n")
-
- lastRequestTime = time.Now()
-}
-
-func message(msg string) {
- fmt.Fprintf(terminal, "%s\r\n", msg)
-}
diff --git a/examples/rtl8720dn/webclient/main.go b/examples/rtl8720dn/webclient/main.go
index 6538412..160ddeb 100644
--- a/examples/rtl8720dn/webclient/main.go
+++ b/examples/rtl8720dn/webclient/main.go
@@ -1,34 +1,32 @@
package main
import (
+ "bufio"
"fmt"
+ "strings"
"time"
"tinygo.org/x/drivers/net"
- "tinygo.org/x/drivers/rtl8720dn"
+ "tinygo.org/x/drivers/net/http"
)
// You can override the setting with the init() in another source code.
// func init() {
// ssid = "your-ssid"
// password = "your-password"
+// url = "http://tinygo.org/"
// debug = true
-// server = "tinygo.org"
// }
var (
ssid string
password string
- server string = "tinygo.org"
- debug = false
+ url = "http://tinygo.org/"
+ debug = false
)
var buf [0x400]byte
-var lastRequestTime time.Time
-var conn net.Conn
-var adaptor *rtl8720dn.RTL8720DN
-
func main() {
err := run()
for err != nil {
@@ -43,6 +41,7 @@ func run() error {
return err
}
net.UseDriver(rtl)
+ http.SetBuf(buf[:])
err = rtl.ConnectToAP(ssid, password)
if err != nil {
@@ -57,60 +56,49 @@ func run() error {
fmt.Printf("Mask : %s\r\n", subnet)
fmt.Printf("Gateway : %s\r\n", gateway)
+ // You can send and receive cookies in the following way
+ // import "tinygo.org/x/drivers/net/http/cookiejar"
+ // jar, err := cookiejar.New(nil)
+ // if err != nil {
+ // return err
+ // }
+ // client := &http.Client{Jar: jar}
+ // http.DefaultClient = client
+
cnt := 0
for {
- readConnection()
- if time.Now().Sub(lastRequestTime).Milliseconds() >= 10000 {
- makeHTTPRequest()
- cnt++
- fmt.Printf("-------- %d --------\r\n", cnt)
+ // Various examples are as follows
+ //
+ // -- Get
+ // resp, err := http.Get(url)
+ //
+ // -- Post
+ // body := `cnt=12`
+ // resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(body))
+ //
+ // -- Post with JSON
+ // body := `{"msg": "hello"}`
+ // resp, err := http.Post(url, "application/json", strings.NewReader(body))
+
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
}
- }
-}
-func readConnection() {
- if conn != nil {
- for n, err := conn.Read(buf[:]); n > 0; n, err = conn.Read(buf[:]) {
- if err != nil {
- println("Read error: " + err.Error())
- } else {
- print(string(buf[0:n]))
- }
+ fmt.Printf("%s %s\r\n", resp.Proto, resp.Status)
+ for k, v := range resp.Header {
+ fmt.Printf("%s: %s\r\n", k, strings.Join(v, " "))
}
+ fmt.Printf("\r\n")
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ fmt.Printf("%s\r\n", scanner.Text())
+ }
+ resp.Body.Close()
+
+ cnt++
+ fmt.Printf("-------- %d --------\r\n", cnt)
+ time.Sleep(10 * time.Second)
}
}
-
-func makeHTTPRequest() {
-
- var err error
- if conn != nil {
- conn.Close()
- }
-
- // make TCP connection
- ip := net.ParseIP(server)
- raddr := &net.TCPAddr{IP: ip, Port: 80}
- laddr := &net.TCPAddr{Port: 8080}
-
- message("\r\n---------------\r\nDialing TCP connection")
- conn, err = net.DialTCP("tcp", laddr, raddr)
- for ; err != nil; conn, err = net.DialTCP("tcp", laddr, raddr) {
- message("Connection failed: " + err.Error())
- time.Sleep(5 * time.Second)
- }
- println("Connected!\r")
-
- print("Sending HTTP request...")
- fmt.Fprintln(conn, "GET / HTTP/1.1")
- fmt.Fprintln(conn, "Host:", server)
- fmt.Fprintln(conn, "User-Agent: TinyGo")
- fmt.Fprintln(conn, "Connection: close")
- fmt.Fprintln(conn)
- println("Sent!\r\n\r")
-
- lastRequestTime = time.Now()
-}
-
-func message(msg string) {
- println(msg, "\r")
-}
diff --git a/examples/rtl8720dn/webserver/main.go b/examples/rtl8720dn/webserver/main.go
index 7c9adaf..639c052 100644
--- a/examples/rtl8720dn/webserver/main.go
+++ b/examples/rtl8720dn/webserver/main.go
@@ -69,6 +69,31 @@ func run() error {
}
func root(w http.ResponseWriter, r *http.Request) {
+ access := 1
+
+ cookie, err := r.Cookie("access")
+ if err != nil {
+ if err == http.ErrNoCookie {
+ cookie = &http.Cookie{
+ Name: "access",
+ Value: "1",
+ }
+ } else {
+ http.Error(w, fmt.Sprintf("%s", err.Error()), http.StatusBadRequest)
+ return
+ }
+ } else {
+ v, err := strconv.ParseInt(cookie.Value, 10, 0)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("invalid cookie.Value : %s", cookie.Value), http.StatusBadRequest)
+ return
+ }
+ cookie.Value = fmt.Sprintf("%d", v+1)
+ access = int(v) + 1
+ }
+ http.SetCookie(w, cookie)
+ w.WriteHeader(http.StatusOK)
+
fmt.Fprintf(w, `
@@ -89,6 +114,11 @@ func root(w http.ResponseWriter, r *http.Request) {
TinyGo HTTP Server
+
+
+ access: %d
+
+
/hello
/6
@@ -110,7 +140,7 @@ func root(w http.ResponseWriter, r *http.Request) {
- `)
+ `, access)
}
func sixlines(w http.ResponseWriter, r *http.Request) {
diff --git a/net/http/client.go b/net/http/client.go
new file mode 100644
index 0000000..e717004
--- /dev/null
+++ b/net/http/client.go
@@ -0,0 +1,214 @@
+package http
+
+import (
+ "io"
+ "time"
+)
+
+// A Client is an HTTP client. Its zero value (DefaultClient) is a
+// usable client that uses DefaultTransport.
+//
+// The Client's Transport typically has internal state (cached TCP
+// connections), so Clients should be reused instead of created as
+// needed. Clients are safe for concurrent use by multiple goroutines.
+//
+// A Client is higher-level than a RoundTripper (such as Transport)
+// and additionally handles HTTP details such as cookies and
+// redirects.
+//
+// When following redirects, the Client will forward all headers set on the
+// initial Request except:
+//
+// • when forwarding sensitive headers like "Authorization",
+// "WWW-Authenticate", and "Cookie" to untrusted targets.
+// These headers will be ignored when following a redirect to a domain
+// that is not a subdomain match or exact match of the initial domain.
+// For example, a redirect from "foo.com" to either "foo.com" or "sub.foo.com"
+// will forward the sensitive headers, but a redirect to "bar.com" will not.
+//
+// • when forwarding the "Cookie" header with a non-nil cookie Jar.
+// Since each redirect may mutate the state of the cookie jar,
+// a redirect may possibly alter a cookie set in the initial request.
+// When forwarding the "Cookie" header, any mutated cookies will be omitted,
+// with the expectation that the Jar will insert those mutated cookies
+// with the updated values (assuming the origin matches).
+// If Jar is nil, the initial cookies are forwarded without change.
+//
+type Client struct {
+ // Transport specifies the mechanism by which individual
+ // HTTP requests are made.
+ // If nil, DefaultTransport is used.
+ Transport RoundTripper
+
+ // CheckRedirect specifies the policy for handling redirects.
+ // If CheckRedirect is not nil, the client calls it before
+ // following an HTTP redirect. The arguments req and via are
+ // the upcoming request and the requests made already, oldest
+ // first. If CheckRedirect returns an error, the Client's Get
+ // method returns both the previous Response (with its Body
+ // closed) and CheckRedirect's error (wrapped in a url.Error)
+ // instead of issuing the Request req.
+ // As a special case, if CheckRedirect returns ErrUseLastResponse,
+ // then the most recent response is returned with its body
+ // unclosed, along with a nil error.
+ //
+ // If CheckRedirect is nil, the Client uses its default policy,
+ // which is to stop after 10 consecutive requests.
+ CheckRedirect func(req *Request, via []*Request) error
+
+ // Jar specifies the cookie jar.
+ //
+ // The Jar is used to insert relevant cookies into every
+ // outbound Request and is updated with the cookie values
+ // of every inbound Response. The Jar is consulted for every
+ // redirect that the Client follows.
+ //
+ // If Jar is nil, cookies are only sent if they are explicitly
+ // set on the Request.
+ Jar CookieJar
+
+ // Timeout specifies a time limit for requests made by this
+ // Client. The timeout includes connection time, any
+ // redirects, and reading the response body. The timer remains
+ // running after Get, Head, Post, or Do return and will
+ // interrupt reading of the Response.Body.
+ //
+ // A Timeout of zero means no timeout.
+ //
+ // The Client cancels requests to the underlying Transport
+ // as if the Request's Context ended.
+ //
+ // For compatibility, the Client will also use the deprecated
+ // CancelRequest method on Transport if found. New
+ // RoundTripper implementations should use the Request's Context
+ // for cancellation instead of implementing CancelRequest.
+ Timeout time.Duration
+}
+
+// DefaultClient is the default Client and is used by Get, Head, and Post.
+var DefaultClient = &Client{}
+
+// RoundTripper is an interface representing the ability to execute a
+// single HTTP transaction, obtaining the Response for a given Request.
+//
+// A RoundTripper must be safe for concurrent use by multiple
+// goroutines.
+type RoundTripper interface {
+ // RoundTrip executes a single HTTP transaction, returning
+ // a Response for the provided Request.
+ //
+ // RoundTrip should not attempt to interpret the response. In
+ // particular, RoundTrip must return err == nil if it obtained
+ // a response, regardless of the response's HTTP status code.
+ // A non-nil err should be reserved for failure to obtain a
+ // response. Similarly, RoundTrip should not attempt to
+ // handle higher-level protocol details such as redirects,
+ // authentication, or cookies.
+ //
+ // RoundTrip should not modify the request, except for
+ // consuming and closing the Request's Body. RoundTrip may
+ // read fields of the request in a separate goroutine. Callers
+ // should not mutate or reuse the request until the Response's
+ // Body has been closed.
+ //
+ // RoundTrip must always close the body, including on errors,
+ // but depending on the implementation may do so in a separate
+ // goroutine even after RoundTrip returns. This means that
+ // callers wanting to reuse the body for subsequent requests
+ // must arrange to wait for the Close call before doing so.
+ //
+ // The Request's URL and Header fields must be initialized.
+ RoundTrip(*Request) (*Response, error)
+}
+
+// Get issues a GET to the specified URL. If the response is one of
+// the following redirect codes, Get follows the redirect, up to a
+// maximum of 10 redirects:
+//
+// 301 (Moved Permanently)
+// 302 (Found)
+// 303 (See Other)
+// 307 (Temporary Redirect)
+// 308 (Permanent Redirect)
+//
+// An error is returned if there were too many redirects or if there
+// was an HTTP protocol error. A non-2xx response doesn't cause an
+// error. Any returned error will be of type *url.Error. The url.Error
+// value's Timeout method will report true if request timed out or was
+// canceled.
+//
+// When err is nil, resp always contains a non-nil resp.Body.
+// Caller should close resp.Body when done reading from it.
+//
+// Get is a wrapper around DefaultClient.Get.
+//
+// To make a request with custom headers, use NewRequest and
+// DefaultClient.Do.
+func Get(url string) (resp *Response, err error) {
+ return DefaultClient.Get(url)
+}
+
+// Get issues a GET to the specified URL. If the response is one of the
+// following redirect codes, Get follows the redirect after calling the
+// Client's CheckRedirect function:
+//
+// 301 (Moved Permanently)
+// 302 (Found)
+// 303 (See Other)
+// 307 (Temporary Redirect)
+// 308 (Permanent Redirect)
+//
+// An error is returned if the Client's CheckRedirect function fails
+// or if there was an HTTP protocol error. A non-2xx response doesn't
+// cause an error. Any returned error will be of type *url.Error. The
+// url.Error value's Timeout method will report true if the request
+// timed out.
+//
+// When err is nil, resp always contains a non-nil resp.Body.
+// Caller should close resp.Body when done reading from it.
+//
+// To make a request with custom headers, use NewRequest and Client.Do.
+func (c *Client) Get(url string) (resp *Response, err error) {
+ req, err := NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ return c.Do(req)
+}
+
+// Post issues a POST to the specified URL.
+//
+// Caller should close resp.Body when done reading from it.
+//
+// If the provided body is an io.Closer, it is closed after the
+// request.
+//
+// Post is a wrapper around DefaultClient.Post.
+//
+// To set custom headers, use NewRequest and DefaultClient.Do.
+//
+// See the Client.Do method documentation for details on how redirects
+// are handled.
+func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
+ return DefaultClient.Post(url, contentType, body)
+}
+
+// Post issues a POST to the specified URL.
+//
+// Caller should close resp.Body when done reading from it.
+//
+// If the provided body is an io.Closer, it is closed after the
+// request.
+//
+// To set custom headers, use NewRequest and Client.Do.
+//
+// See the Client.Do method documentation for details on how redirects
+// are handled.
+func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
+ req, err := NewRequest("POST", url, body)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", contentType)
+ return c.Do(req)
+}
diff --git a/net/http/cookie.go b/net/http/cookie.go
new file mode 100644
index 0000000..141bc94
--- /dev/null
+++ b/net/http/cookie.go
@@ -0,0 +1,433 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+ "log"
+ "net"
+ "net/textproto"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
+// HTTP response or the Cookie header of an HTTP request.
+//
+// See https://tools.ietf.org/html/rfc6265 for details.
+type Cookie struct {
+ Name string
+ Value string
+
+ Path string // optional
+ Domain string // optional
+ Expires time.Time // optional
+ RawExpires string // for reading cookies only
+
+ // MaxAge=0 means no 'Max-Age' attribute specified.
+ // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
+ // MaxAge>0 means Max-Age attribute present and given in seconds
+ MaxAge int
+ Secure bool
+ HttpOnly bool
+ SameSite SameSite
+ Raw string
+ Unparsed []string // Raw text of unparsed attribute-value pairs
+}
+
+// SameSite allows a server to define a cookie attribute making it impossible for
+// the browser to send this cookie along with cross-site requests. The main
+// goal is to mitigate the risk of cross-origin information leakage, and provide
+// some protection against cross-site request forgery attacks.
+//
+// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
+type SameSite int
+
+const (
+ SameSiteDefaultMode SameSite = iota + 1
+ SameSiteLaxMode
+ SameSiteStrictMode
+ SameSiteNoneMode
+)
+
+// readSetCookies parses all "Set-Cookie" values from
+// the header h and returns the successfully parsed Cookies.
+func readSetCookies(h Header) []*Cookie {
+ cookieCount := len(h["Set-Cookie"])
+ if cookieCount == 0 {
+ return []*Cookie{}
+ }
+ cookies := make([]*Cookie, 0, cookieCount)
+ for _, line := range h["Set-Cookie"] {
+ parts := strings.Split(textproto.TrimString(line), ";")
+ if len(parts) == 1 && parts[0] == "" {
+ continue
+ }
+ parts[0] = textproto.TrimString(parts[0])
+ j := strings.Index(parts[0], "=")
+ if j < 0 {
+ continue
+ }
+ name, value := parts[0][:j], parts[0][j+1:]
+ if !isCookieNameValid(name) {
+ continue
+ }
+ value, ok := parseCookieValue(value, true)
+ if !ok {
+ continue
+ }
+ c := &Cookie{
+ Name: name,
+ Value: value,
+ Raw: line,
+ }
+ for i := 1; i < len(parts); i++ {
+ parts[i] = textproto.TrimString(parts[i])
+ if len(parts[i]) == 0 {
+ continue
+ }
+
+ attr, val := parts[i], ""
+ if j := strings.Index(attr, "="); j >= 0 {
+ attr, val = attr[:j], attr[j+1:]
+ }
+ lowerAttr := strings.ToLower(attr)
+ val, ok = parseCookieValue(val, false)
+ if !ok {
+ c.Unparsed = append(c.Unparsed, parts[i])
+ continue
+ }
+ switch lowerAttr {
+ case "samesite":
+ lowerVal := strings.ToLower(val)
+ switch lowerVal {
+ case "lax":
+ c.SameSite = SameSiteLaxMode
+ case "strict":
+ c.SameSite = SameSiteStrictMode
+ case "none":
+ c.SameSite = SameSiteNoneMode
+ default:
+ c.SameSite = SameSiteDefaultMode
+ }
+ continue
+ case "secure":
+ c.Secure = true
+ continue
+ case "httponly":
+ c.HttpOnly = true
+ continue
+ case "domain":
+ c.Domain = val
+ continue
+ case "max-age":
+ secs, err := strconv.Atoi(val)
+ if err != nil || secs != 0 && val[0] == '0' {
+ break
+ }
+ if secs <= 0 {
+ secs = -1
+ }
+ c.MaxAge = secs
+ continue
+ case "expires":
+ c.RawExpires = val
+ exptime, err := time.Parse(time.RFC1123, val)
+ if err != nil {
+ exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
+ if err != nil {
+ c.Expires = time.Time{}
+ break
+ }
+ }
+ c.Expires = exptime.UTC()
+ continue
+ case "path":
+ c.Path = val
+ continue
+ }
+ c.Unparsed = append(c.Unparsed, parts[i])
+ }
+ cookies = append(cookies, c)
+ }
+ return cookies
+}
+
+// SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.
+// The provided cookie must have a valid Name. Invalid cookies may be
+// silently dropped.
+func SetCookie(w ResponseWriter, cookie *Cookie) {
+ if v := cookie.String(); v != "" {
+ w.Header().Add("Set-Cookie", v)
+ }
+}
+
+// String returns the serialization of the cookie for use in a Cookie
+// header (if only Name and Value are set) or a Set-Cookie response
+// header (if other fields are set).
+// If c is nil or c.Name is invalid, the empty string is returned.
+func (c *Cookie) String() string {
+ if c == nil || !isCookieNameValid(c.Name) {
+ return ""
+ }
+ // extraCookieLength derived from typical length of cookie attributes
+ // see RFC 6265 Sec 4.1.
+ const extraCookieLength = 110
+ var b strings.Builder
+ b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength)
+ b.WriteString(c.Name)
+ b.WriteRune('=')
+ b.WriteString(sanitizeCookieValue(c.Value))
+
+ if len(c.Path) > 0 {
+ b.WriteString("; Path=")
+ b.WriteString(sanitizeCookiePath(c.Path))
+ }
+ if len(c.Domain) > 0 {
+ if validCookieDomain(c.Domain) {
+ // A c.Domain containing illegal characters is not
+ // sanitized but simply dropped which turns the cookie
+ // into a host-only cookie. A leading dot is okay
+ // but won't be sent.
+ d := c.Domain
+ if d[0] == '.' {
+ d = d[1:]
+ }
+ b.WriteString("; Domain=")
+ b.WriteString(d)
+ } else {
+ log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c.Domain)
+ }
+ }
+ var buf [len(TimeFormat)]byte
+ if validCookieExpires(c.Expires) {
+ b.WriteString("; Expires=")
+ b.Write(c.Expires.UTC().AppendFormat(buf[:0], TimeFormat))
+ }
+ if c.MaxAge > 0 {
+ b.WriteString("; Max-Age=")
+ b.Write(strconv.AppendInt(buf[:0], int64(c.MaxAge), 10))
+ } else if c.MaxAge < 0 {
+ b.WriteString("; Max-Age=0")
+ }
+ if c.HttpOnly {
+ b.WriteString("; HttpOnly")
+ }
+ if c.Secure {
+ b.WriteString("; Secure")
+ }
+ switch c.SameSite {
+ case SameSiteDefaultMode:
+ // Skip, default mode is obtained by not emitting the attribute.
+ case SameSiteNoneMode:
+ b.WriteString("; SameSite=None")
+ case SameSiteLaxMode:
+ b.WriteString("; SameSite=Lax")
+ case SameSiteStrictMode:
+ b.WriteString("; SameSite=Strict")
+ }
+ return b.String()
+}
+
+// readCookies parses all "Cookie" values from the header h and
+// returns the successfully parsed Cookies.
+//
+// if filter isn't empty, only cookies of that name are returned
+func readCookies(h Header, filter string) []*Cookie {
+ lines := h["Cookie"]
+ if len(lines) == 0 {
+ return []*Cookie{}
+ }
+
+ cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
+ for _, line := range lines {
+ line = textproto.TrimString(line)
+
+ var part string
+ for len(line) > 0 { // continue since we have rest
+ if splitIndex := strings.Index(line, ";"); splitIndex > 0 {
+ part, line = line[:splitIndex], line[splitIndex+1:]
+ } else {
+ part, line = line, ""
+ }
+ part = textproto.TrimString(part)
+ if len(part) == 0 {
+ continue
+ }
+ name, val := part, ""
+ if j := strings.Index(part, "="); j >= 0 {
+ name, val = name[:j], name[j+1:]
+ }
+ if !isCookieNameValid(name) {
+ continue
+ }
+ if filter != "" && filter != name {
+ continue
+ }
+ val, ok := parseCookieValue(val, true)
+ if !ok {
+ continue
+ }
+ cookies = append(cookies, &Cookie{Name: name, Value: val})
+ }
+ }
+ return cookies
+}
+
+// validCookieDomain reports whether v is a valid cookie domain-value.
+func validCookieDomain(v string) bool {
+ if isCookieDomainName(v) {
+ return true
+ }
+ if net.ParseIP(v) != nil && !strings.Contains(v, ":") {
+ return true
+ }
+ return false
+}
+
+// validCookieExpires reports whether v is a valid cookie expires-value.
+func validCookieExpires(t time.Time) bool {
+ // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601
+ return t.Year() >= 1601
+}
+
+// isCookieDomainName reports whether s is a valid domain name or a valid
+// domain name with a leading dot '.'. It is almost a direct copy of
+// package net's isDomainName.
+func isCookieDomainName(s string) bool {
+ if len(s) == 0 {
+ return false
+ }
+ if len(s) > 255 {
+ return false
+ }
+
+ if s[0] == '.' {
+ // A cookie a domain attribute may start with a leading dot.
+ s = s[1:]
+ }
+ last := byte('.')
+ ok := false // Ok once we've seen a letter.
+ partlen := 0
+ for i := 0; i < len(s); i++ {
+ c := s[i]
+ switch {
+ default:
+ return false
+ case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
+ // No '_' allowed here (in contrast to package net).
+ ok = true
+ partlen++
+ case '0' <= c && c <= '9':
+ // fine
+ partlen++
+ case c == '-':
+ // Byte before dash cannot be dot.
+ if last == '.' {
+ return false
+ }
+ partlen++
+ case c == '.':
+ // Byte before dot cannot be dot, dash.
+ if last == '.' || last == '-' {
+ return false
+ }
+ if partlen > 63 || partlen == 0 {
+ return false
+ }
+ partlen = 0
+ }
+ last = c
+ }
+ if last == '-' || partlen > 63 {
+ return false
+ }
+
+ return ok
+}
+
+var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
+
+func sanitizeCookieName(n string) string {
+ return cookieNameSanitizer.Replace(n)
+}
+
+// sanitizeCookieValue produces a suitable cookie-value from v.
+// https://tools.ietf.org/html/rfc6265#section-4.1.1
+// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+// ; US-ASCII characters excluding CTLs,
+// ; whitespace DQUOTE, comma, semicolon,
+// ; and backslash
+// We loosen this as spaces and commas are common in cookie values
+// but we produce a quoted cookie-value if and only if v contains
+// commas or spaces.
+// See https://golang.org/issue/7243 for the discussion.
+func sanitizeCookieValue(v string) string {
+ v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
+ if len(v) == 0 {
+ return v
+ }
+ if strings.IndexByte(v, ' ') >= 0 || strings.IndexByte(v, ',') >= 0 {
+ return `"` + v + `"`
+ }
+ return v
+}
+
+func validCookieValueByte(b byte) bool {
+ return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
+}
+
+// path-av = "Path=" path-value
+// path-value =
+func sanitizeCookiePath(v string) string {
+ return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
+}
+
+func validCookiePathByte(b byte) bool {
+ return 0x20 <= b && b < 0x7f && b != ';'
+}
+
+func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
+ ok := true
+ for i := 0; i < len(v); i++ {
+ if valid(v[i]) {
+ continue
+ }
+ log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
+ ok = false
+ break
+ }
+ if ok {
+ return v
+ }
+ buf := make([]byte, 0, len(v))
+ for i := 0; i < len(v); i++ {
+ if b := v[i]; valid(b) {
+ buf = append(buf, b)
+ }
+ }
+ return string(buf)
+}
+
+func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) {
+ // Strip the quotes, if present.
+ if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
+ raw = raw[1 : len(raw)-1]
+ }
+ for i := 0; i < len(raw); i++ {
+ if !validCookieValueByte(raw[i]) {
+ return "", false
+ }
+ }
+ return raw, true
+}
+
+func isCookieNameValid(raw string) bool {
+ if raw == "" {
+ return false
+ }
+ return strings.IndexFunc(raw, isNotToken) < 0
+}
diff --git a/net/http/cookiejar/jar.go b/net/http/cookiejar/jar.go
new file mode 100644
index 0000000..15cef58
--- /dev/null
+++ b/net/http/cookiejar/jar.go
@@ -0,0 +1,504 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar.
+package cookiejar
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "tinygo.org/x/drivers/net"
+ "tinygo.org/x/drivers/net/http"
+)
+
+// PublicSuffixList provides the public suffix of a domain. For example:
+// - the public suffix of "example.com" is "com",
+// - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and
+// - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".
+//
+// Implementations of PublicSuffixList must be safe for concurrent use by
+// multiple goroutines.
+//
+// An implementation that always returns "" is valid and may be useful for
+// testing but it is not secure: it means that the HTTP server for foo.com can
+// set a cookie for bar.com.
+//
+// A public suffix list implementation is in the package
+// golang.org/x/net/publicsuffix.
+type PublicSuffixList interface {
+ // PublicSuffix returns the public suffix of domain.
+ //
+ // TODO: specify which of the caller and callee is responsible for IP
+ // addresses, for leading and trailing dots, for case sensitivity, and
+ // for IDN/Punycode.
+ PublicSuffix(domain string) string
+
+ // String returns a description of the source of this public suffix
+ // list. The description will typically contain something like a time
+ // stamp or version number.
+ String() string
+}
+
+// Options are the options for creating a new Jar.
+type Options struct {
+ // PublicSuffixList is the public suffix list that determines whether
+ // an HTTP server can set a cookie for a domain.
+ //
+ // A nil value is valid and may be useful for testing but it is not
+ // secure: it means that the HTTP server for foo.co.uk can set a cookie
+ // for bar.co.uk.
+ PublicSuffixList PublicSuffixList
+}
+
+// Jar implements the http.CookieJar interface from the net/http package.
+type Jar struct {
+ psList PublicSuffixList
+
+ // mu locks the remaining fields.
+ mu sync.Mutex
+
+ // entries is a set of entries, keyed by their eTLD+1 and subkeyed by
+ // their name/domain/path.
+ entries map[string]map[string]entry
+
+ // nextSeqNum is the next sequence number assigned to a new cookie
+ // created SetCookies.
+ nextSeqNum uint64
+}
+
+// New returns a new cookie jar. A nil *Options is equivalent to a zero
+// Options.
+func New(o *Options) (*Jar, error) {
+ jar := &Jar{
+ entries: make(map[string]map[string]entry),
+ }
+ if o != nil {
+ jar.psList = o.PublicSuffixList
+ }
+ return jar, nil
+}
+
+// entry is the internal representation of a cookie.
+//
+// This struct type is not used outside of this package per se, but the exported
+// fields are those of RFC 6265.
+type entry struct {
+ Name string
+ Value string
+ Domain string
+ Path string
+ SameSite string
+ Secure bool
+ HttpOnly bool
+ Persistent bool
+ HostOnly bool
+ Expires time.Time
+ Creation time.Time
+ LastAccess time.Time
+
+ // seqNum is a sequence number so that Cookies returns cookies in a
+ // deterministic order, even for cookies that have equal Path length and
+ // equal Creation time. This simplifies testing.
+ seqNum uint64
+}
+
+// id returns the domain;path;name triple of e as an id.
+func (e *entry) id() string {
+ return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)
+}
+
+// shouldSend determines whether e's cookie qualifies to be included in a
+// request to host/path. It is the caller's responsibility to check if the
+// cookie is expired.
+func (e *entry) shouldSend(https bool, host, path string) bool {
+ return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
+}
+
+// domainMatch implements "domain-match" of RFC 6265 section 5.1.3.
+func (e *entry) domainMatch(host string) bool {
+ if e.Domain == host {
+ return true
+ }
+ return !e.HostOnly && hasDotSuffix(host, e.Domain)
+}
+
+// pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
+func (e *entry) pathMatch(requestPath string) bool {
+ if requestPath == e.Path {
+ return true
+ }
+ if strings.HasPrefix(requestPath, e.Path) {
+ if e.Path[len(e.Path)-1] == '/' {
+ return true // The "/any/" matches "/any/path" case.
+ } else if requestPath[len(e.Path)] == '/' {
+ return true // The "/any" matches "/any/path" case.
+ }
+ }
+ return false
+}
+
+// hasDotSuffix reports whether s ends in "."+suffix.
+func hasDotSuffix(s, suffix string) bool {
+ return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
+}
+
+// Cookies implements the Cookies method of the http.CookieJar interface.
+//
+// It returns an empty slice if the URL's scheme is not HTTP or HTTPS.
+func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
+ return j.cookies(u, time.Now())
+}
+
+// cookies is like Cookies but takes the current time as a parameter.
+func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
+ if u.Scheme != "http" && u.Scheme != "https" {
+ return cookies
+ }
+ host, err := canonicalHost(u.Host)
+ if err != nil {
+ return cookies
+ }
+ key := jarKey(host, j.psList)
+
+ j.mu.Lock()
+ defer j.mu.Unlock()
+
+ submap := j.entries[key]
+ if submap == nil {
+ return cookies
+ }
+
+ https := u.Scheme == "https"
+ path := u.Path
+ if path == "" {
+ path = "/"
+ }
+
+ modified := false
+ var selected []entry
+ for id, e := range submap {
+ if e.Persistent && !e.Expires.After(now) {
+ delete(submap, id)
+ modified = true
+ continue
+ }
+ if !e.shouldSend(https, host, path) {
+ continue
+ }
+ e.LastAccess = now
+ submap[id] = e
+ selected = append(selected, e)
+ modified = true
+ }
+ if modified {
+ if len(submap) == 0 {
+ delete(j.entries, key)
+ } else {
+ j.entries[key] = submap
+ }
+ }
+
+ // sort according to RFC 6265 section 5.4 point 2: by longest
+ // path and then by earliest creation time.
+ sort.Slice(selected, func(i, j int) bool {
+ s := selected
+ if len(s[i].Path) != len(s[j].Path) {
+ return len(s[i].Path) > len(s[j].Path)
+ }
+ if !s[i].Creation.Equal(s[j].Creation) {
+ return s[i].Creation.Before(s[j].Creation)
+ }
+ return s[i].seqNum < s[j].seqNum
+ })
+ for _, e := range selected {
+ cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
+ }
+
+ return cookies
+}
+
+// SetCookies implements the SetCookies method of the http.CookieJar interface.
+//
+// It does nothing if the URL's scheme is not HTTP or HTTPS.
+func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
+ j.setCookies(u, cookies, time.Now())
+}
+
+// setCookies is like SetCookies but takes the current time as parameter.
+func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
+ if len(cookies) == 0 {
+ return
+ }
+ if u.Scheme != "http" && u.Scheme != "https" {
+ return
+ }
+ host, err := canonicalHost(u.Host)
+ if err != nil {
+ return
+ }
+ key := jarKey(host, j.psList)
+ defPath := defaultPath(u.Path)
+
+ j.mu.Lock()
+ defer j.mu.Unlock()
+
+ submap := j.entries[key]
+
+ modified := false
+ for _, cookie := range cookies {
+ e, remove, err := j.newEntry(cookie, now, defPath, host)
+ if err != nil {
+ continue
+ }
+ id := e.id()
+ if remove {
+ if submap != nil {
+ if _, ok := submap[id]; ok {
+ delete(submap, id)
+ modified = true
+ }
+ }
+ continue
+ }
+ if submap == nil {
+ submap = make(map[string]entry)
+ }
+
+ if old, ok := submap[id]; ok {
+ e.Creation = old.Creation
+ e.seqNum = old.seqNum
+ } else {
+ e.Creation = now
+ e.seqNum = j.nextSeqNum
+ j.nextSeqNum++
+ }
+ e.LastAccess = now
+ submap[id] = e
+ modified = true
+ }
+
+ if modified {
+ if len(submap) == 0 {
+ delete(j.entries, key)
+ } else {
+ j.entries[key] = submap
+ }
+ }
+}
+
+// canonicalHost strips port from host if present and returns the canonicalized
+// host name.
+func canonicalHost(host string) (string, error) {
+ var err error
+ host = strings.ToLower(host)
+ if hasPort(host) {
+ host, _, err = net.SplitHostPort(host)
+ if err != nil {
+ return "", err
+ }
+ }
+ if strings.HasSuffix(host, ".") {
+ // Strip trailing dot from fully qualified domain names.
+ host = host[:len(host)-1]
+ }
+ return toASCII(host)
+}
+
+// hasPort reports whether host contains a port number. host may be a host
+// name, an IPv4 or an IPv6 address.
+func hasPort(host string) bool {
+ colons := strings.Count(host, ":")
+ if colons == 0 {
+ return false
+ }
+ if colons == 1 {
+ return true
+ }
+ return host[0] == '[' && strings.Contains(host, "]:")
+}
+
+// jarKey returns the key to use for a jar.
+func jarKey(host string, psl PublicSuffixList) string {
+ if isIP(host) {
+ return host
+ }
+
+ var i int
+ if psl == nil {
+ i = strings.LastIndex(host, ".")
+ if i <= 0 {
+ return host
+ }
+ } else {
+ suffix := psl.PublicSuffix(host)
+ if suffix == host {
+ return host
+ }
+ i = len(host) - len(suffix)
+ if i <= 0 || host[i-1] != '.' {
+ // The provided public suffix list psl is broken.
+ // Storing cookies under host is a safe stopgap.
+ return host
+ }
+ // Only len(suffix) is used to determine the jar key from
+ // here on, so it is okay if psl.PublicSuffix("www.buggy.psl")
+ // returns "com" as the jar key is generated from host.
+ }
+ prevDot := strings.LastIndex(host[:i-1], ".")
+ return host[prevDot+1:]
+}
+
+// isIP reports whether host is an IP address.
+func isIP(host string) bool {
+ return net.ParseIP(host) != nil
+}
+
+// defaultPath returns the directory part of an URL's path according to
+// RFC 6265 section 5.1.4.
+func defaultPath(path string) string {
+ if len(path) == 0 || path[0] != '/' {
+ return "/" // Path is empty or malformed.
+ }
+
+ i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.
+ if i == 0 {
+ return "/" // Path has the form "/abc".
+ }
+ return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".
+}
+
+// newEntry creates an entry from a http.Cookie c. now is the current time and
+// is compared to c.Expires to determine deletion of c. defPath and host are the
+// default-path and the canonical host name of the URL c was received from.
+//
+// remove records whether the jar should delete this cookie, as it has already
+// expired with respect to now. In this case, e may be incomplete, but it will
+// be valid to call e.id (which depends on e's Name, Domain and Path).
+//
+// A malformed c.Domain will result in an error.
+func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {
+ e.Name = c.Name
+
+ if c.Path == "" || c.Path[0] != '/' {
+ e.Path = defPath
+ } else {
+ e.Path = c.Path
+ }
+
+ e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
+ if err != nil {
+ return e, false, err
+ }
+
+ // MaxAge takes precedence over Expires.
+ if c.MaxAge < 0 {
+ return e, true, nil
+ } else if c.MaxAge > 0 {
+ e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
+ e.Persistent = true
+ } else {
+ if c.Expires.IsZero() {
+ e.Expires = endOfTime
+ e.Persistent = false
+ } else {
+ if !c.Expires.After(now) {
+ return e, true, nil
+ }
+ e.Expires = c.Expires
+ e.Persistent = true
+ }
+ }
+
+ e.Value = c.Value
+ e.Secure = c.Secure
+ e.HttpOnly = c.HttpOnly
+
+ switch c.SameSite {
+ case http.SameSiteDefaultMode:
+ e.SameSite = "SameSite"
+ case http.SameSiteStrictMode:
+ e.SameSite = "SameSite=Strict"
+ case http.SameSiteLaxMode:
+ e.SameSite = "SameSite=Lax"
+ }
+
+ return e, false, nil
+}
+
+var (
+ errIllegalDomain = errors.New("cookiejar: illegal cookie domain attribute")
+ errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")
+ errNoHostname = errors.New("cookiejar: no host name available (IP only)")
+)
+
+// endOfTime is the time when session (non-persistent) cookies expire.
+// This instant is representable in most date/time formats (not just
+// Go's time.Time) and should be far enough in the future.
+var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
+
+// domainAndType determines the cookie's domain and hostOnly attribute.
+func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
+ if domain == "" {
+ // No domain attribute in the SetCookie header indicates a
+ // host cookie.
+ return host, true, nil
+ }
+
+ if isIP(host) {
+ // According to RFC 6265 domain-matching includes not being
+ // an IP address.
+ // TODO: This might be relaxed as in common browsers.
+ return "", false, errNoHostname
+ }
+
+ // From here on: If the cookie is valid, it is a domain cookie (with
+ // the one exception of a public suffix below).
+ // See RFC 6265 section 5.2.3.
+ if domain[0] == '.' {
+ domain = domain[1:]
+ }
+
+ if len(domain) == 0 || domain[0] == '.' {
+ // Received either "Domain=." or "Domain=..some.thing",
+ // both are illegal.
+ return "", false, errMalformedDomain
+ }
+ domain = strings.ToLower(domain)
+
+ if domain[len(domain)-1] == '.' {
+ // We received stuff like "Domain=www.example.com.".
+ // Browsers do handle such stuff (actually differently) but
+ // RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
+ // requiring a reject. 4.1.2.3 is not normative, but
+ // "Domain Matching" (5.1.3) and "Canonicalized Host Names"
+ // (5.1.2) are.
+ return "", false, errMalformedDomain
+ }
+
+ // See RFC 6265 section 5.3 #5.
+ if j.psList != nil {
+ if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
+ if host == domain {
+ // This is the one exception in which a cookie
+ // with a domain attribute is a host cookie.
+ return host, true, nil
+ }
+ return "", false, errIllegalDomain
+ }
+ }
+
+ // The domain must domain-match host: www.mycompany.com cannot
+ // set cookies for .ourcompetitors.com.
+ if host != domain && !hasDotSuffix(host, domain) {
+ return "", false, errIllegalDomain
+ }
+
+ return domain, false, nil
+}
diff --git a/net/http/cookiejar/punycode.go b/net/http/cookiejar/punycode.go
new file mode 100644
index 0000000..a9cc666
--- /dev/null
+++ b/net/http/cookiejar/punycode.go
@@ -0,0 +1,159 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cookiejar
+
+// This file implements the Punycode algorithm from RFC 3492.
+
+import (
+ "fmt"
+ "strings"
+ "unicode/utf8"
+)
+
+// These parameter values are specified in section 5.
+//
+// All computation is done with int32s, so that overflow behavior is identical
+// regardless of whether int is 32-bit or 64-bit.
+const (
+ base int32 = 36
+ damp int32 = 700
+ initialBias int32 = 72
+ initialN int32 = 128
+ skew int32 = 38
+ tmax int32 = 26
+ tmin int32 = 1
+)
+
+// encode encodes a string as specified in section 6.3 and prepends prefix to
+// the result.
+//
+// The "while h < length(input)" line in the specification becomes "for
+// remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes.
+func encode(prefix, s string) (string, error) {
+ output := make([]byte, len(prefix), len(prefix)+1+2*len(s))
+ copy(output, prefix)
+ delta, n, bias := int32(0), initialN, initialBias
+ b, remaining := int32(0), int32(0)
+ for _, r := range s {
+ if r < utf8.RuneSelf {
+ b++
+ output = append(output, byte(r))
+ } else {
+ remaining++
+ }
+ }
+ h := b
+ if b > 0 {
+ output = append(output, '-')
+ }
+ for remaining != 0 {
+ m := int32(0x7fffffff)
+ for _, r := range s {
+ if m > r && r >= n {
+ m = r
+ }
+ }
+ delta += (m - n) * (h + 1)
+ if delta < 0 {
+ return "", fmt.Errorf("cookiejar: invalid label %q", s)
+ }
+ n = m
+ for _, r := range s {
+ if r < n {
+ delta++
+ if delta < 0 {
+ return "", fmt.Errorf("cookiejar: invalid label %q", s)
+ }
+ continue
+ }
+ if r > n {
+ continue
+ }
+ q := delta
+ for k := base; ; k += base {
+ t := k - bias
+ if t < tmin {
+ t = tmin
+ } else if t > tmax {
+ t = tmax
+ }
+ if q < t {
+ break
+ }
+ output = append(output, encodeDigit(t+(q-t)%(base-t)))
+ q = (q - t) / (base - t)
+ }
+ output = append(output, encodeDigit(q))
+ bias = adapt(delta, h+1, h == b)
+ delta = 0
+ h++
+ remaining--
+ }
+ delta++
+ n++
+ }
+ return string(output), nil
+}
+
+func encodeDigit(digit int32) byte {
+ switch {
+ case 0 <= digit && digit < 26:
+ return byte(digit + 'a')
+ case 26 <= digit && digit < 36:
+ return byte(digit + ('0' - 26))
+ }
+ panic("cookiejar: internal error in punycode encoding")
+}
+
+// adapt is the bias adaptation function specified in section 6.1.
+func adapt(delta, numPoints int32, firstTime bool) int32 {
+ if firstTime {
+ delta /= damp
+ } else {
+ delta /= 2
+ }
+ delta += delta / numPoints
+ k := int32(0)
+ for delta > ((base-tmin)*tmax)/2 {
+ delta /= base - tmin
+ k += base
+ }
+ return k + (base-tmin+1)*delta/(delta+skew)
+}
+
+// Strictly speaking, the remaining code below deals with IDNA (RFC 5890 and
+// friends) and not Punycode (RFC 3492) per se.
+
+// acePrefix is the ASCII Compatible Encoding prefix.
+const acePrefix = "xn--"
+
+// toASCII converts a domain or domain label to its ASCII form. For example,
+// toASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
+// toASCII("golang") is "golang".
+func toASCII(s string) (string, error) {
+ if ascii(s) {
+ return s, nil
+ }
+ labels := strings.Split(s, ".")
+ for i, label := range labels {
+ if !ascii(label) {
+ a, err := encode(acePrefix, label)
+ if err != nil {
+ return "", err
+ }
+ labels[i] = a
+ }
+ }
+ return strings.Join(labels, "."), nil
+}
+
+func ascii(s string) bool {
+ for i := 0; i < len(s); i++ {
+ if s[i] >= utf8.RuneSelf {
+ return false
+ }
+ }
+ return true
+}
diff --git a/net/http/jar.go b/net/http/jar.go
new file mode 100644
index 0000000..5c3de0d
--- /dev/null
+++ b/net/http/jar.go
@@ -0,0 +1,27 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+ "net/url"
+)
+
+// A CookieJar manages storage and use of cookies in HTTP requests.
+//
+// Implementations of CookieJar must be safe for concurrent use by multiple
+// goroutines.
+//
+// The net/http/cookiejar package provides a CookieJar implementation.
+type CookieJar interface {
+ // SetCookies handles the receipt of the cookies in a reply for the
+ // given URL. It may or may not choose to save the cookies, depending
+ // on the jar's policy and implementation.
+ SetCookies(u *url.URL, cookies []*Cookie)
+
+ // Cookies returns the cookies to send in a request for the given URL.
+ // It is up to the implementation to honor the standard cookie use
+ // restrictions such as in RFC 6265.
+ Cookies(u *url.URL) []*Cookie
+}
diff --git a/net/http/request.go b/net/http/request.go
index 58756ea..ac67be2 100644
--- a/net/http/request.go
+++ b/net/http/request.go
@@ -2,6 +2,7 @@ package http
import (
"bufio"
+ "bytes"
"context"
"crypto/tls"
"errors"
@@ -11,6 +12,7 @@ import (
"mime/multipart"
"net/textproto"
"net/url"
+ urlpkg "net/url"
"strconv"
"strings"
"sync"
@@ -248,6 +250,57 @@ func (r *Request) ProtoAtLeast(major, minor int) bool {
r.ProtoMajor == major && r.ProtoMinor >= minor
}
+// UserAgent returns the client's User-Agent, if sent in the request.
+func (r *Request) UserAgent() string {
+ return r.Header.Get("User-Agent")
+}
+
+// Cookies parses and returns the HTTP cookies sent with the request.
+func (r *Request) Cookies() []*Cookie {
+ return readCookies(r.Header, "")
+}
+
+// ErrNoCookie is returned by Request's Cookie method when a cookie is not found.
+var ErrNoCookie = errors.New("http: named cookie not present")
+
+// Cookie returns the named cookie provided in the request or
+// ErrNoCookie if not found.
+// If multiple cookies match the given name, only one cookie will
+// be returned.
+func (r *Request) Cookie(name string) (*Cookie, error) {
+ for _, c := range readCookies(r.Header, name) {
+ return c, nil
+ }
+ return nil, ErrNoCookie
+}
+
+// AddCookie adds a cookie to the request. Per RFC 6265 section 5.4,
+// AddCookie does not attach more than one Cookie header field. That
+// means all cookies, if any, are written into the same line,
+// separated by semicolon.
+// AddCookie only sanitizes c's name and value, and does not sanitize
+// a Cookie header already present in the request.
+func (r *Request) AddCookie(c *Cookie) {
+ s := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
+ if c := r.Header.Get("Cookie"); c != "" {
+ r.Header.Set("Cookie", c+"; "+s)
+ } else {
+ r.Header.Set("Cookie", s)
+ }
+}
+
+// Referer returns the referring URL, if sent in the request.
+//
+// Referer is misspelled as in the request itself, a mistake from the
+// earliest days of HTTP. This value can also be fetched from the
+// Header map as Header["Referer"]; the benefit of making it available
+// as a method is that the compiler can diagnose programs that use the
+// alternate (correct English) spelling req.Referrer() but cannot
+// diagnose programs that use Header["Referrer"].
+func (r *Request) Referer() string {
+ return r.Header.Get("Referer")
+}
+
// isH2Upgrade reports whether r represents the http2 "client preface"
// magic string.
func (r *Request) isH2Upgrade() bool {
@@ -299,6 +352,114 @@ func validMethod(method string) bool {
return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1
}
+// NewRequest wraps NewRequestWithContext using the background context.
+func NewRequest(method, url string, body io.Reader) (*Request, error) {
+ return NewRequestWithContext(context.Background(), method, url, body)
+}
+
+// NewRequestWithContext returns a new Request given a method, URL, and
+// optional body.
+//
+// If the provided body is also an io.Closer, the returned
+// Request.Body is set to body and will be closed by the Client
+// methods Do, Post, and PostForm, and Transport.RoundTrip.
+//
+// NewRequestWithContext returns a Request suitable for use with
+// Client.Do or Transport.RoundTrip. To create a request for use with
+// testing a Server Handler, either use the NewRequest function in the
+// net/http/httptest package, use ReadRequest, or manually update the
+// Request fields. For an outgoing client request, the context
+// controls the entire lifetime of a request and its response:
+// obtaining a connection, sending the request, and reading the
+// response headers and body. See the Request type's documentation for
+// the difference between inbound and outbound request fields.
+//
+// If body is of type *bytes.Buffer, *bytes.Reader, or
+// *strings.Reader, the returned request's ContentLength is set to its
+// exact value (instead of -1), GetBody is populated (so 307 and 308
+// redirects can replay the body), and Body is set to NoBody if the
+// ContentLength is 0.
+func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
+ if method == "" {
+ // We document that "" means "GET" for Request.Method, and people have
+ // relied on that from NewRequest, so keep that working.
+ // We still enforce validMethod for non-empty methods.
+ method = "GET"
+ }
+ if !validMethod(method) {
+ return nil, fmt.Errorf("net/http: invalid method %q", method)
+ }
+ if ctx == nil {
+ return nil, errors.New("net/http: nil Context")
+ }
+ u, err := urlpkg.Parse(url)
+ if err != nil {
+ return nil, err
+ }
+ rc, ok := body.(io.ReadCloser)
+ if !ok && body != nil {
+ rc = io.NopCloser(body)
+ }
+ // The host's colon:port should be normalized. See Issue 14836.
+ u.Host = removeEmptyPort(u.Host)
+ req := &Request{
+ ctx: ctx,
+ Method: method,
+ URL: u,
+ Proto: "HTTP/1.1",
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Header: make(Header),
+ Body: rc,
+ Host: u.Host,
+ }
+ if body != nil {
+ switch v := body.(type) {
+ case *bytes.Buffer:
+ req.ContentLength = int64(v.Len())
+ buf := v.Bytes()
+ req.GetBody = func() (io.ReadCloser, error) {
+ r := bytes.NewReader(buf)
+ return io.NopCloser(r), nil
+ }
+ case *bytes.Reader:
+ req.ContentLength = int64(v.Len())
+ snapshot := *v
+ req.GetBody = func() (io.ReadCloser, error) {
+ r := snapshot
+ return io.NopCloser(&r), nil
+ }
+ case *strings.Reader:
+ req.ContentLength = int64(v.Len())
+ snapshot := *v
+ req.GetBody = func() (io.ReadCloser, error) {
+ r := snapshot
+ return io.NopCloser(&r), nil
+ }
+ default:
+ // This is where we'd set it to -1 (at least
+ // if body != NoBody) to mean unknown, but
+ // that broke people during the Go 1.8 testing
+ // period. People depend on it being 0 I
+ // guess. Maybe retry later. See Issue 18117.
+ }
+ // For client requests, Request.ContentLength of 0
+ // means either actually 0, or unknown. The only way
+ // to explicitly say that the ContentLength is zero is
+ // to set the Body to nil. But turns out too much code
+ // depends on NewRequest returning a non-nil Body,
+ // so we use a well-known ReadCloser variable instead
+ // and have the http package also treat that sentinel
+ // variable to mean explicitly zero.
+ if req.GetBody != nil && req.ContentLength == 0 {
+ req.Body = NoBody
+ req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
+ }
+ }
+
+ return req, nil
+}
+
// parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
s1 := strings.Index(line, " ")
diff --git a/net/http/response.go b/net/http/response.go
index 508ae39..d49f054 100644
--- a/net/http/response.go
+++ b/net/http/response.go
@@ -99,6 +99,11 @@ type Response struct {
TLS *tls.ConnectionState
}
+// Cookies parses and returns the cookies set in the Set-Cookie headers.
+func (r *Response) Cookies() []*Cookie {
+ return readSetCookies(r.Header)
+}
+
// RFC 7234, section 5.4: Should treat
// Pragma: no-cache
// like
diff --git a/net/http/tinygo.go b/net/http/tinygo.go
new file mode 100644
index 0000000..c533127
--- /dev/null
+++ b/net/http/tinygo.go
@@ -0,0 +1,287 @@
+package http
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+
+ "tinygo.org/x/drivers/net"
+ "tinygo.org/x/drivers/net/tls"
+)
+
+var buf []byte
+
+func SetBuf(b []byte) {
+ buf = b
+}
+
+func (c *Client) Do(req *Request) (*Response, error) {
+ switch req.URL.Scheme {
+ case "http":
+ return c.doHTTP(req)
+ case "https":
+ return c.doHTTPS(req)
+ default:
+ return nil, fmt.Errorf("invalid schemer : %s", req.URL.Scheme)
+ }
+}
+
+func (c *Client) doHTTP(req *Request) (*Response, error) {
+ if c.Jar != nil {
+ for _, cookie := range c.Jar.Cookies(req.URL) {
+ req.AddCookie(cookie)
+ }
+ }
+
+ // make TCP connection
+ ip := net.ParseIP(req.URL.Host)
+ raddr := &net.TCPAddr{IP: ip, Port: 80}
+ laddr := &net.TCPAddr{Port: 8080}
+
+ conn, err := net.DialTCP("tcp", laddr, raddr)
+ retry := 0
+ for ; err != nil; conn, err = net.DialTCP("tcp", laddr, raddr) {
+ retry++
+ if retry > 10 {
+ return nil, fmt.Errorf("Connection failed: %s", err.Error())
+ }
+ time.Sleep(1 * time.Second)
+ }
+
+ p := req.URL.Path
+ if p == "" {
+ p = "/"
+ }
+ fmt.Fprintln(conn, req.Method+" "+p+" HTTP/1.1")
+ fmt.Fprintln(conn, "Host:", req.URL.Host)
+
+ if req.Header.get(`User-Agent`) == "" {
+ fmt.Fprintln(conn, "User-Agent: TinyGo")
+ }
+
+ for k, v := range req.Header {
+ if v == nil || len(v) == 0 {
+ return nil, fmt.Errorf("req.Header error: %s", k)
+ }
+ fmt.Fprintln(conn, k+": "+v[0])
+ }
+
+ if req.Header.get(`Connection`) == "" {
+ fmt.Fprintln(conn, "Connection: close")
+ }
+
+ if req.ContentLength > 0 {
+ fmt.Fprintf(conn, "Content-Length: %d\n", req.ContentLength)
+ }
+
+ fmt.Fprintln(conn)
+
+ if req.ContentLength > 0 {
+ b, err := req.GetBody()
+ if err != nil {
+ return nil, err
+ }
+
+ n, err := b.Read(buf)
+ if err != nil {
+ return nil, err
+ }
+ conn.Write(buf[:n])
+
+ b.Close()
+
+ }
+
+ return c.doResp(conn, req)
+}
+
+func (c *Client) doHTTPS(req *Request) (*Response, error) {
+ conn, err := tls.Dial("tcp", req.URL.Host, nil)
+ retry := 0
+ for ; err != nil; conn, err = tls.Dial("tcp", req.URL.Host, nil) {
+ retry++
+ if retry > 10 {
+ return nil, fmt.Errorf("Connection failed: %s", err.Error())
+ }
+ time.Sleep(1 * time.Second)
+ }
+
+ p := req.URL.Path
+ if p == "" {
+ p = "/"
+ }
+ fmt.Fprintln(conn, req.Method+" "+p+" HTTP/1.1")
+ fmt.Fprintln(conn, "Host:", req.URL.Host)
+
+ if req.Header.get(`User-Agent`) == "" {
+ fmt.Fprintln(conn, "User-Agent: TinyGo")
+ }
+
+ for k, v := range req.Header {
+ if v == nil || len(v) == 0 {
+ return nil, fmt.Errorf("req.Header error: %s", k)
+ }
+ fmt.Fprintln(conn, k+": "+v[0])
+ }
+
+ if req.Header.get(`Connection`) == "" {
+ fmt.Fprintln(conn, "Connection: close")
+ }
+
+ if req.ContentLength > 0 {
+ fmt.Fprintf(conn, "Content-Length: %d\n", req.ContentLength)
+ }
+
+ fmt.Fprintln(conn)
+
+ if req.ContentLength > 0 {
+ b, err := req.GetBody()
+ if err != nil {
+ return nil, err
+ }
+
+ n, err := b.Read(buf)
+ if err != nil {
+ return nil, err
+ }
+ conn.Write(buf[:n])
+
+ b.Close()
+
+ }
+
+ return c.doResp(conn, req)
+}
+
+func (c *Client) doResp(conn net.Conn, req *Request) (*Response, error) {
+ resp := &Response{
+ Header: map[string][]string{},
+ }
+
+ // Header
+ var scanner *bufio.Scanner
+ cont := true
+ ofs := 0
+ remain := int64(0)
+ for cont {
+ for n, err := conn.Read(buf[ofs:]); n > 0; n, err = conn.Read(buf[ofs:]) {
+ if err != nil {
+ println("Read error: " + err.Error())
+ } else {
+ idx := bytes.Index(buf[ofs:ofs+n], []byte("\r\n\r\n"))
+ if idx == -1 {
+ ofs += n
+ continue
+ }
+ idx += ofs + 4
+
+ scanner = bufio.NewScanner(bytes.NewReader(buf[0 : ofs+n]))
+ if resp.Status == "" && scanner.Scan() {
+ status := strings.SplitN(scanner.Text(), " ", 2)
+ if len(status) != 2 {
+ conn.Close()
+ return nil, fmt.Errorf("invalid status : %q", scanner.Text())
+ }
+ resp.Proto = status[0]
+ fmt.Sscanf(status[0], "HTTP/%d.%d", &resp.ProtoMajor, &resp.ProtoMinor)
+
+ resp.Status = status[1]
+ fmt.Sscanf(status[1], "%d", &resp.StatusCode)
+ }
+
+ for scanner.Scan() {
+ text := scanner.Text()
+ if text == "" {
+ // end of header
+ if idx < n+ofs {
+ ofs = ofs + n - idx
+ for i := 0; i < ofs; i++ {
+ buf[i] = buf[i+idx]
+ }
+ } else {
+ ofs = 0
+ }
+ break
+ } else {
+ header := strings.SplitN(text, ": ", 2)
+ if len(header) != 2 {
+ conn.Close()
+ return nil, fmt.Errorf("invalid header : %q", text)
+ }
+ if resp.Header.Get(header[0]) == "" {
+ resp.Header.Set(header[0], header[1])
+ } else {
+ resp.Header.Add(header[0], header[1])
+ }
+
+ if header[0] == "Content-Length" {
+ resp.ContentLength, err = strconv.ParseInt(header[1], 10, 64)
+ if err != nil {
+ conn.Close()
+ return nil, err
+ }
+ remain = resp.ContentLength
+ }
+ }
+ }
+ cont = false
+ break
+ }
+ }
+ }
+
+ // Body
+ remain -= int64(ofs)
+ if remain <= 0 {
+ resp.Body = io.NopCloser(bytes.NewReader(buf[:ofs]))
+ if c.Jar != nil {
+ if rc := resp.Cookies(); len(rc) > 0 {
+ c.Jar.SetCookies(req.URL, rc)
+ }
+ }
+ return resp, conn.Close()
+ }
+
+ cont = true
+ lastRequestTime := time.Now()
+ for cont {
+ for {
+ end := ofs + 0x400
+ if len(buf) < end {
+ return nil, fmt.Errorf("slice out of range : use http.SetBuf() to change the allocation to %d bytes or more", end)
+ }
+ n, err := conn.Read(buf[ofs : ofs+0x400])
+ if n == 0 {
+ continue
+ }
+ if err != nil {
+ conn.Close()
+ return nil, err
+ } else {
+ ofs += n
+ remain -= int64(n)
+ if remain <= 0 {
+ resp.Body = io.NopCloser(bytes.NewReader(buf[:ofs]))
+ cont = false
+ break
+ }
+ if time.Now().Sub(lastRequestTime).Milliseconds() >= 1000 {
+ conn.Close()
+ return nil, fmt.Errorf("time out")
+ }
+ }
+ }
+ }
+
+ if c.Jar != nil {
+ if rc := resp.Cookies(); len(rc) > 0 {
+ c.Jar.SetCookies(req.URL, rc)
+ }
+ }
+
+ return resp, conn.Close()
+}
diff --git a/net/ipsocki.go b/net/ipsocki.go
new file mode 100644
index 0000000..dfc30c9
--- /dev/null
+++ b/net/ipsocki.go
@@ -0,0 +1,26 @@
+package net
+
+import "strings"
+
+// SplitHostPort splits a network address of the form "host:port",
+// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or
+// host%zone and port.
+//
+// A literal IPv6 address in hostport must be enclosed in square
+// brackets, as in "[::1]:80", "[::1%lo0]:80".
+//
+// See func Dial for a description of the hostport parameter, and host
+// and port results.
+func SplitHostPort(hostport string) (host, port string, err error) {
+
+ if strings.Contains(hostport, ":") {
+ spl := strings.Split(hostport, ":")
+ host = spl[0]
+ port = spl[1]
+ } else {
+ host = hostport
+ port = "80"
+ }
+
+ return host, port, nil
+}
diff --git a/rtl8720dn/netdriver.go b/rtl8720dn/netdriver.go
index 3a55fde..fc79b52 100644
--- a/rtl8720dn/netdriver.go
+++ b/rtl8720dn/netdriver.go
@@ -226,7 +226,12 @@ func (r *RTL8720DN) ReadSocket(b []byte) (n int, err error) {
switch r.connectionType {
case ConnectionTypeTCP:
- nn, err := r.Rpc_lwip_recv(r.socket, &b, uint32(len(b)), 0x00000008, 0x00002800)
+ length := len(b)
+ if length > maxUartRecvSize-16 {
+ length = maxUartRecvSize - 16
+ }
+ buf := b[:length]
+ nn, err := r.Rpc_lwip_recv(r.socket, &buf, uint32(length), 0x00000008, 0x00002800)
if err != nil {
return 0, err
}
@@ -236,17 +241,14 @@ func (r *RTL8720DN) ReadSocket(b []byte) (n int, err error) {
} else if nn == 0 {
return 0, r.DisconnectSocket()
}
- if r.length == 0 {
- header := httpHeader(b[:nn])
- r.length = header.ContentLength()
- }
- r.length -= int(nn)
- if r.length == 0 {
- return int(nn), r.DisconnectSocket()
- }
n = int(nn)
case ConnectionTypeTLS:
- nn, err := r.Rpc_wifi_get_ssl_receive(r.client, &b, int32(len(b)))
+ length := len(b)
+ if length > maxUartRecvSize-16 {
+ length = maxUartRecvSize - 16
+ }
+ buf := b[:length]
+ nn, err := r.Rpc_wifi_get_ssl_receive(r.client, &buf, int32(length))
if err != nil {
return 0, err
}
diff --git a/rtl8720dn/rtl8720dn.go b/rtl8720dn/rtl8720dn.go
index d43eb2c..84508e9 100644
--- a/rtl8720dn/rtl8720dn.go
+++ b/rtl8720dn/rtl8720dn.go
@@ -2,6 +2,8 @@ package rtl8720dn
import "io"
+const maxUartRecvSize = 128
+
type RTL8720DN struct {
port io.ReadWriter
seq uint64