aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/huin/goupnp/httpu/httpu.go
blob: f52dad68b10236eac5dd5eb257b0db3b321608f7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package httpu

import (
    "bufio"
    "bytes"
    "errors"
    "fmt"
    "log"
    "net"
    "net/http"
    "sync"
    "time"
)

// HTTPUClient is a client for dealing with HTTPU (HTTP over UDP). Its typical
// function is for HTTPMU, and particularly SSDP.
type HTTPUClient struct {
    connLock sync.Mutex // Protects use of conn.
    conn     net.PacketConn
}

// NewHTTPUClient creates a new HTTPUClient, opening up a new UDP socket for the
// purpose.
func NewHTTPUClient() (*HTTPUClient, error) {
    conn, err := net.ListenPacket("udp", ":0")
    if err != nil {
        return nil, err
    }
    return &HTTPUClient{conn: conn}, nil
}

// NewHTTPUClientAddr creates a new HTTPUClient which will broadcast packets
// from the specified address, opening up a new UDP socket for the purpose
func NewHTTPUClientAddr(addr string) (*HTTPUClient, error) {
    ip := net.ParseIP(addr)
    if ip == nil {
        return nil, errors.New("Invalid listening address")
    }
    conn, err := net.ListenPacket("udp", ip.String()+":0")
    if err != nil {
        return nil, err
    }
    return &HTTPUClient{conn: conn}, nil
}

// Close shuts down the client. The client will no longer be useful following
// this.
func (httpu *HTTPUClient) Close() error {
    httpu.connLock.Lock()
    defer httpu.connLock.Unlock()
    return httpu.conn.Close()
}

// Do performs a request. The timeout is how long to wait for before returning
// the responses that were received. An error is only returned for failing to
// send the request. Failures in receipt simply do not add to the resulting
// responses.
//
// Note that at present only one concurrent connection will happen per
// HTTPUClient.
func (httpu *HTTPUClient) Do(req *http.Request, timeout time.Duration, numSends int) ([]*http.Response, error) {
    httpu.connLock.Lock()
    defer httpu.connLock.Unlock()

    // Create the request. This is a subset of what http.Request.Write does
    // deliberately to avoid creating extra fields which may confuse some
    // devices.
    var requestBuf bytes.Buffer
    method := req.Method
    if method == "" {
        method = "GET"
    }
    if _, err := fmt.Fprintf(&requestBuf, "%s %s HTTP/1.1\r\n", method, req.URL.RequestURI()); err != nil {
        return nil, err
    }
    if err := req.Header.Write(&requestBuf); err != nil {
        return nil, err
    }
    if _, err := requestBuf.Write([]byte{'\r', '\n'}); err != nil {
        return nil, err
    }

    destAddr, err := net.ResolveUDPAddr("udp", req.Host)
    if err != nil {
        return nil, err
    }
    if err = httpu.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
        return nil, err
    }

    // Send request.
    for i := 0; i < numSends; i++ {
        if n, err := httpu.conn.WriteTo(requestBuf.Bytes(), destAddr); err != nil {
            return nil, err
        } else if n < len(requestBuf.Bytes()) {
            return nil, fmt.Errorf("httpu: wrote %d bytes rather than full %d in request",
                n, len(requestBuf.Bytes()))
        }
        time.Sleep(5 * time.Millisecond)
    }

    // Await responses until timeout.
    var responses []*http.Response
    responseBytes := make([]byte, 2048)
    for {
        // 2048 bytes should be sufficient for most networks.
        n, _, err := httpu.conn.ReadFrom(responseBytes)
        if err != nil {
            if err, ok := err.(net.Error); ok {
                if err.Timeout() {
                    break
                }
                if err.Temporary() {
                    // Sleep in case this is a persistent error to avoid pegging CPU until deadline.
                    time.Sleep(10 * time.Millisecond)
                    continue
                }
            }
            return nil, err
        }

        // Parse response.
        response, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(responseBytes[:n])), req)
        if err != nil {
            log.Print("httpu: error while parsing response: %v", err)
            continue
        }

        responses = append(responses, response)
    }
    return responses, err
}