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