mirror of
https://github.com/golang/go
synced 2024-11-26 16:36:49 -07:00
early HTTP library and trivial server
R=r OCL=15777 CL=15791
This commit is contained in:
parent
a61bb95497
commit
42b7789a92
59
src/lib/http/Makefile
Normal file
59
src/lib/http/Makefile
Normal file
@ -0,0 +1,59 @@
|
||||
# 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.
|
||||
|
||||
# DO NOT EDIT. Automatically generated by gobuild.
|
||||
# gobuild -m http conn.go request.go server.go url.go
|
||||
O=6
|
||||
GC=$(O)g
|
||||
CC=$(O)c -w
|
||||
AS=$(O)a
|
||||
AR=$(O)ar
|
||||
|
||||
PKG=$(GOROOT)/pkg/http.a
|
||||
|
||||
install: $(PKG)
|
||||
|
||||
nuke: clean
|
||||
rm -f $(PKG)
|
||||
|
||||
clean:
|
||||
rm -f *.$O *.a
|
||||
|
||||
%.$O: %.go
|
||||
$(GC) $*.go
|
||||
|
||||
%.$O: %.c
|
||||
$(CC) $*.c
|
||||
|
||||
%.$O: %.s
|
||||
$(AS) $*.s
|
||||
|
||||
|
||||
O1=\
|
||||
url.$O\
|
||||
|
||||
O2=\
|
||||
request.$O\
|
||||
|
||||
O3=\
|
||||
conn.$O\
|
||||
|
||||
O4=\
|
||||
server.$O\
|
||||
|
||||
$(PKG): a1 a2 a3 a4
|
||||
a1: $(O1)
|
||||
$(AR) grc $(PKG) $(O1)
|
||||
a2: $(O2)
|
||||
$(AR) grc $(PKG) $(O2)
|
||||
a3: $(O3)
|
||||
$(AR) grc $(PKG) $(O3)
|
||||
a4: $(O4)
|
||||
$(AR) grc $(PKG) $(O4)
|
||||
|
||||
$(O1): nuke
|
||||
$(O2): a1
|
||||
$(O3): a2
|
||||
$(O4): a3
|
||||
|
60
src/lib/http/conn.go
Normal file
60
src/lib/http/conn.go
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 (
|
||||
"io";
|
||||
"bufio";
|
||||
"http";
|
||||
"os"
|
||||
)
|
||||
|
||||
// Read/write/close interface.
|
||||
type RWC interface {
|
||||
Read(p *[]byte) (n int, err *os.Error);
|
||||
Write(p *[]byte) (n int, err *os.Error);
|
||||
Close() *os.Error
|
||||
}
|
||||
|
||||
// Active HTTP connection (server side).
|
||||
export type Conn struct {
|
||||
rwc RWC;
|
||||
br *bufio.BufRead;
|
||||
bw *bufio.BufWrite;
|
||||
close bool;
|
||||
chunking bool;
|
||||
}
|
||||
|
||||
// Create new connection from rwc.
|
||||
export func NewConn(rwc RWC) (c *Conn, err *os.Error) {
|
||||
c = new(Conn);
|
||||
c.rwc = rwc;
|
||||
if c.br, err = bufio.NewBufRead(rwc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.bw, err = bufio.NewBufWrite(rwc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Read next request from connection.
|
||||
func (c *Conn) ReadRequest() (req *Request, err *os.Error) {
|
||||
if req, err = ReadRequest(c.br); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Proper handling of (lack of) Connection: close,
|
||||
// and chunked transfer encoding on output.
|
||||
c.close = true
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Close the connection.
|
||||
func (c *Conn) Close() {
|
||||
c.bw.Flush();
|
||||
c.rwc.Close();
|
||||
}
|
||||
|
300
src/lib/http/request.go
Normal file
300
src/lib/http/request.go
Normal file
@ -0,0 +1,300 @@
|
||||
// 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.
|
||||
|
||||
// HTTP Request reading and parsing.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio";
|
||||
"http";
|
||||
"os";
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxLineLength = 1024; // assumed < bufio.DefaultBufSize
|
||||
MaxValueLength = 1024;
|
||||
MaxHeaderLines = 1024;
|
||||
)
|
||||
|
||||
export var (
|
||||
LineTooLong = os.NewError("http header line too long");
|
||||
ValueTooLong = os.NewError("http header value too long");
|
||||
HeaderTooLong = os.NewError("http header too long");
|
||||
BadHeader = os.NewError("malformed http header");
|
||||
BadRequest = os.NewError("invalid http request");
|
||||
BadHTTPVersion = os.NewError("unsupported http version");
|
||||
)
|
||||
|
||||
// HTTP Request
|
||||
export type Request struct {
|
||||
method string; // GET, PUT,etc.
|
||||
rawurl string;
|
||||
url *URL; // URI after GET, PUT etc.
|
||||
proto string; // "HTTP/1.0"
|
||||
pmajor int; // 1
|
||||
pminor int; // 0
|
||||
|
||||
header *map[string] string;
|
||||
|
||||
close bool;
|
||||
host string;
|
||||
referer string;
|
||||
useragent string;
|
||||
}
|
||||
|
||||
// Read a line of bytes (up to \n) from b.
|
||||
// Give up if the line exceeds MaxLineLength.
|
||||
// The returned bytes are a pointer into storage in
|
||||
// the bufio, so they are only valid until the next bufio read.
|
||||
func ReadLineBytes(b *bufio.BufRead) (p *[]byte, err *os.Error) {
|
||||
if p, err = b.ReadLineSlice('\n'); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p) >= MaxLineLength {
|
||||
return nil, LineTooLong
|
||||
}
|
||||
|
||||
// Chop off trailing white space.
|
||||
var i int
|
||||
for i = len(p); i > 0; i-- {
|
||||
if c := p[i-1]; c != ' ' && c != '\r' && c != '\t' && c != '\n' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return p[0:i], nil
|
||||
}
|
||||
|
||||
// ReadLineByte, but convert the bytes into a string.
|
||||
func ReadLine(b *bufio.BufRead) (s string, err *os.Error) {
|
||||
p, e := ReadLineBytes(b)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return string(p), nil
|
||||
}
|
||||
|
||||
// Read a key/value pair from b.
|
||||
// A key/value has the form Key: Value\r\n
|
||||
// and the Value can continue on multiple lines if each continuation line
|
||||
// starts with a space.
|
||||
func ReadKeyValue(b *bufio.BufRead) (key, value string, err *os.Error) {
|
||||
line, e := ReadLineBytes(b)
|
||||
if e != nil {
|
||||
return "", "", e
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// Scan first line for colon.
|
||||
for i := 0; i < len(line); i++ {
|
||||
switch line[i] {
|
||||
case ' ':
|
||||
// Key field has space - no good.
|
||||
return "", "", BadHeader
|
||||
case ':':
|
||||
key = string(line[0:i]);
|
||||
// Skip initial space before value.
|
||||
for i++; i < len(line); i++ {
|
||||
if line[i] != ' ' {
|
||||
break
|
||||
}
|
||||
}
|
||||
value = string(line[i:len(line)])
|
||||
|
||||
// Look for extension lines, which must begin with space.
|
||||
for {
|
||||
var c byte;
|
||||
|
||||
if c, e = b.ReadByte(); e != nil {
|
||||
return "", "", e
|
||||
}
|
||||
if c != ' ' {
|
||||
// Not leading space; stop.
|
||||
b.UnreadByte()
|
||||
break
|
||||
}
|
||||
|
||||
// Eat leading space.
|
||||
for c == ' ' {
|
||||
if c, e = b.ReadByte(); e != nil {
|
||||
return "", "", e
|
||||
}
|
||||
}
|
||||
b.UnreadByte()
|
||||
|
||||
// Read the rest of the line and add to value.
|
||||
if line, e = ReadLineBytes(b); e != nil {
|
||||
return "", "", e
|
||||
}
|
||||
value += " " + string(line)
|
||||
|
||||
if len(value) >= MaxValueLength {
|
||||
return "", "", ValueTooLong
|
||||
}
|
||||
}
|
||||
return key, value, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Line ended before space or colon.
|
||||
return "", "", BadHeader;
|
||||
}
|
||||
|
||||
// Convert decimal at s[i:len(s)] to integer,
|
||||
// returning value, string position where the digits stopped,
|
||||
// and whether there was a valid number (digits, not too big).
|
||||
func atoi(s string, i int) (n, i1 int, ok bool) {
|
||||
const Big = 1000000
|
||||
if i >= len(s) || s[i] < '0' || s[i] > '9' {
|
||||
return 0, 0, false
|
||||
}
|
||||
n = 0
|
||||
for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
|
||||
n = n*10 + int(s[i]-'0')
|
||||
if n > Big {
|
||||
return 0, 0, false
|
||||
}
|
||||
}
|
||||
return n, i, true
|
||||
}
|
||||
|
||||
// Parse HTTP version: "HTTP/1.2" -> (1, 2, true).
|
||||
func ParseHTTPVersion(vers string) (int, int, bool) {
|
||||
if vers[0:5] != "HTTP/" {
|
||||
return 0, 0, false
|
||||
}
|
||||
major, i, ok := atoi(vers, 5)
|
||||
if !ok || i >= len(vers) || vers[i] != '.' {
|
||||
return 0, 0, false
|
||||
}
|
||||
var minor int;
|
||||
minor, i, ok = atoi(vers, i+1)
|
||||
if !ok || i != len(vers) {
|
||||
return 0, 0, false
|
||||
}
|
||||
return major, minor, true
|
||||
}
|
||||
|
||||
// Read and parse a request from b.
|
||||
export func ReadRequest(b *bufio.BufRead) (req *Request, err *os.Error) {
|
||||
req = new(Request);
|
||||
|
||||
// First line: GET /index.html HTTP/1.0
|
||||
var s string
|
||||
if s, err = ReadLine(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var f *[]string
|
||||
if f = strings.split(s, " "); len(f) != 3 {
|
||||
return nil, BadRequest
|
||||
}
|
||||
req.method, req.rawurl, req.proto = f[0], f[1], f[2]
|
||||
var ok bool;
|
||||
if req.pmajor, req.pminor, ok = ParseHTTPVersion(req.proto); !ok {
|
||||
return nil, BadHTTPVersion
|
||||
}
|
||||
|
||||
if req.url, err = ParseURL(req.rawurl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subsequent lines: Key: value.
|
||||
nheader := 0;
|
||||
req.header = new(map[string] string)
|
||||
for {
|
||||
var key, value string
|
||||
if key, value, err = ReadKeyValue(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key == "" {
|
||||
break
|
||||
}
|
||||
if nheader++; nheader >= MaxHeaderLines {
|
||||
return nil, HeaderTooLong
|
||||
}
|
||||
|
||||
// RFC 2616 says that if you send the same header key
|
||||
// multiple times, it has to be semantically equivalent
|
||||
// to concatenating the values separated by commas.
|
||||
oldvalue, present := req.header[key]
|
||||
if present {
|
||||
req.header[key] = oldvalue+","+value
|
||||
} else {
|
||||
req.header[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// RFC2616: Must treat
|
||||
// GET /index.html HTTP/1.1
|
||||
// Host: www.google.com
|
||||
// and
|
||||
// GET http://www.google.com/index.html HTTP/1.1
|
||||
// Host: doesntmatter
|
||||
// the same. In the second case, any Host line is ignored.
|
||||
if v, have := req.header["Host"]; have && req.url.host == "" {
|
||||
req.host = v
|
||||
}
|
||||
|
||||
// RFC2616: Should treat
|
||||
// Pragma: no-cache
|
||||
// like
|
||||
// Cache-control: no-cache
|
||||
if v, have := req.header["Pragma"]; have && v == "no-cache" {
|
||||
if cc, havecc := req.header["Cache-control"]; !havecc {
|
||||
req.header["Cache-control"] = "no-cache"
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether to hang up after sending the reply.
|
||||
if req.pmajor < 1 || (req.pmajor == 1 && req.pminor < 1) {
|
||||
req.close = true
|
||||
} else if v, have := req.header["Connection"]; have {
|
||||
// TODO: Should split on commas, toss surrounding white space,
|
||||
// and check each field.
|
||||
if v == "close" {
|
||||
req.close = true
|
||||
}
|
||||
}
|
||||
|
||||
// Pull out useful fields as a convenience to clients.
|
||||
if v, have := req.header["Referer"]; have {
|
||||
req.referer = v
|
||||
}
|
||||
if v, have := req.header["User-Agent"]; have {
|
||||
req.useragent = v
|
||||
}
|
||||
|
||||
|
||||
// TODO: Parse specific header values:
|
||||
// Accept
|
||||
// Accept-Encoding
|
||||
// Accept-Language
|
||||
// Authorization
|
||||
// Cache-Control
|
||||
// Connection
|
||||
// Date
|
||||
// Expect
|
||||
// From
|
||||
// If-Match
|
||||
// If-Modified-Since
|
||||
// If-None-Match
|
||||
// If-Range
|
||||
// If-Unmodified-Since
|
||||
// Max-Forwards
|
||||
// Proxy-Authorization
|
||||
// Referer [sic]
|
||||
// TE (transfer-codings)
|
||||
// Trailer
|
||||
// Transfer-Encoding
|
||||
// Upgrade
|
||||
// User-Agent
|
||||
// Via
|
||||
// Warning
|
||||
|
||||
return req, nil;
|
||||
}
|
65
src/lib/http/server.go
Normal file
65
src/lib/http/server.go
Normal file
@ -0,0 +1,65 @@
|
||||
// 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.
|
||||
|
||||
// Trivial HTTP server
|
||||
|
||||
// TODO: Routines for writing responses.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"io";
|
||||
"os";
|
||||
"net";
|
||||
"http";
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Serve a new connection.
|
||||
func ServeConnection(fd net.Conn, raddr string, f *(*Conn, *Request)) {
|
||||
c, err := NewConn(fd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
req, err := c.ReadRequest()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
f(c, req)
|
||||
if c.close {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Close();
|
||||
}
|
||||
|
||||
// Web server: already listening on l, call f for each request.
|
||||
export func Serve(l net.Listener, f *(*Conn, *Request)) *os.Error {
|
||||
// TODO: Make this unnecessary
|
||||
s, e := os.Getenv("GOMAXPROCS");
|
||||
if n, ok := strings.atoi(s); n < 3 {
|
||||
print("Warning: $GOMAXPROCS needs to be at least 3.\n");
|
||||
}
|
||||
|
||||
for {
|
||||
rw, raddr, e := l.Accept()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
go ServeConnection(rw, raddr, f)
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// Web server: listen on address, call f for each request.
|
||||
export func ListenAndServe(addr string, f *(*Conn, *Request)) *os.Error {
|
||||
l, e := net.Listen("tcp", addr)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
e = Serve(l, f);
|
||||
l.Close()
|
||||
return e
|
||||
}
|
30
src/lib/http/triv.go
Normal file
30
src/lib/http/triv.go
Normal file
@ -0,0 +1,30 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"io";
|
||||
"bufio";
|
||||
"os";
|
||||
"net";
|
||||
"http"
|
||||
)
|
||||
|
||||
func Echo(conn *http.Conn, req *http.Request) {
|
||||
fd := conn.bw;
|
||||
conn.close = true;
|
||||
io.WriteString(fd, "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"\r\n");
|
||||
io.WriteString(fd, req.method+" "+req.rawurl+" "+req.proto+"\r\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := http.ListenAndServe("0.0.0.0:12345", &Echo)
|
||||
if err != nil {
|
||||
panic("ListenAndServe: ", err.String())
|
||||
}
|
||||
}
|
||||
|
178
src/lib/http/url.go
Normal file
178
src/lib/http/url.go
Normal file
@ -0,0 +1,178 @@
|
||||
// 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.
|
||||
|
||||
// Parse URLs (actually URIs, but that seems overly pedantic).
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
export var (
|
||||
BadURL = os.NewError("bad url syntax")
|
||||
)
|
||||
|
||||
func IsHex(c byte) bool {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return true
|
||||
case 'a' <= c && c <= 'f':
|
||||
return true
|
||||
case 'A' <= c && c <= 'F':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func UnHex(c byte) byte {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return c - '0'
|
||||
case 'a' <= c && c <= 'f':
|
||||
return c - 'a' + 10
|
||||
case 'A' <= c && c <= 'F':
|
||||
return c - 'A' + 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Unescape %xx into hex.
|
||||
export func URLUnescape(s string) (string, *os.Error) {
|
||||
// Count %, check that they're well-formed.
|
||||
n := 0
|
||||
for i := 0; i < len(s); {
|
||||
if s[i] == '%' {
|
||||
n++
|
||||
if !IsHex(s[i+1]) || !IsHex(s[i+2]) {
|
||||
return "", BadURL
|
||||
}
|
||||
i += 3
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
t := new([]byte, len(s)-2*n);
|
||||
j := 0
|
||||
for i := 0; i < len(s); {
|
||||
if s[i] == '%' {
|
||||
t[j] = UnHex(s[i+1]) << 4 | UnHex(s[i+2]);
|
||||
j++
|
||||
i += 3
|
||||
} else {
|
||||
t[j] = s[i];
|
||||
j++
|
||||
i++
|
||||
}
|
||||
}
|
||||
return string(t), nil
|
||||
}
|
||||
|
||||
export type URL struct {
|
||||
raw string;
|
||||
scheme string;
|
||||
rawpath string;
|
||||
authority string;
|
||||
userinfo string;
|
||||
host string;
|
||||
path string;
|
||||
query string;
|
||||
fragment string;
|
||||
}
|
||||
|
||||
// Maybe rawurl is of the form scheme:path.
|
||||
// (Scheme must be [a-zA-Z][a-zA-Z0-9+-.]*)
|
||||
// If so, return scheme, path; else return "", rawurl.
|
||||
func GetScheme(rawurl string) (scheme, path string, err *os.Error) {
|
||||
for i := 0; i < len(rawurl); i++ {
|
||||
c := rawurl[i];
|
||||
switch {
|
||||
case 'a' <= c && c <= 'z' ||'A' <= c && c <= 'Z':
|
||||
// do nothing
|
||||
case '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.':
|
||||
if i == 0 {
|
||||
return "", rawurl, nil
|
||||
}
|
||||
case c == ':':
|
||||
if i == 0 {
|
||||
return "", "", BadURL
|
||||
}
|
||||
return rawurl[0:i], rawurl[i+1:len(rawurl)], nil
|
||||
}
|
||||
}
|
||||
return "", rawurl, nil
|
||||
}
|
||||
|
||||
// Maybe s is of the form t c u.
|
||||
// If so, return t, c u (or t, u if cutc == true).
|
||||
// If not, return s, "".
|
||||
func Split(s string, c byte, cutc bool) (string, string) {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == c {
|
||||
if cutc {
|
||||
return s[0:i], s[i+1:len(s)]
|
||||
}
|
||||
return s[0:i], s[i:len(s)]
|
||||
}
|
||||
}
|
||||
return s, ""
|
||||
}
|
||||
|
||||
// Parse rawurl into a URL structure.
|
||||
export func ParseURL(rawurl string) (url *URL, err *os.Error) {
|
||||
if rawurl == "" {
|
||||
return nil, BadURL
|
||||
}
|
||||
url = new(URL);
|
||||
url.raw = rawurl
|
||||
|
||||
// Split off possible leading "http:", "mailto:", etc.
|
||||
var path string
|
||||
if url.scheme, path, err = GetScheme(rawurl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url.rawpath = path
|
||||
|
||||
// RFC 2396: a relative URI (no scheme) has a ?query,
|
||||
// but absolute URIs only have query if path begins with /
|
||||
if url.scheme == "" || len(path) > 0 && path[0] == '/' {
|
||||
path, url.query = Split(path, '?', true);
|
||||
if url.query, err = URLUnescape(url.query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe path is //authority/path
|
||||
if len(path) > 2 && path[0:2] == "//" {
|
||||
url.authority, path = Split(path[2:len(path)], '/', false)
|
||||
}
|
||||
url.userinfo, url.host = Split(url.authority, '@', true);
|
||||
|
||||
// What's left is the path.
|
||||
// TODO: Canonicalize (remove . and ..)?
|
||||
if url.path, err = URLUnescape(url.path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// A URL reference is a URL with #frag potentially added. Parse it.
|
||||
export func ParseURLReference(rawurlref string) (url *URL, err *os.Error) {
|
||||
// Cut off #frag.
|
||||
rawurl, frag := Split(rawurlref, '#', true);
|
||||
if url, err = ParseURL(rawurl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if url.fragment, err = URLUnescape(frag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user