Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/pkg/ports/served-ports.go
2500 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package ports
6
7
import (
8
"bufio"
9
"bytes"
10
"context"
11
"encoding/hex"
12
"fmt"
13
"io"
14
"net"
15
"os"
16
"sort"
17
"strconv"
18
"strings"
19
"time"
20
21
"github.com/gitpod-io/gitpod/common-go/log"
22
)
23
24
// ServedPort describes a port served by a local service.
25
type ServedPort struct {
26
Address net.IP
27
Port uint32
28
BoundToLocalhost bool
29
}
30
31
// ServedPortsObserver observes the locally served ports and provides
32
// full updates whenever that list changes.
33
type ServedPortsObserver interface {
34
// Observe starts observing the served ports until the context is canceled.
35
// The list of served ports is always the complete picture, i.e. if a single port changes,
36
// the whole list is returned.
37
// When the observer stops operating (because the context as canceled or an irrecoverable
38
// error occurred), the observer will close both channels.
39
Observe(ctx context.Context) (<-chan []ServedPort, <-chan error)
40
}
41
42
const (
43
maxSubscriptions = 100
44
45
fnNetTCP = "/proc/net/tcp"
46
fnNetTCP6 = "/proc/net/tcp6"
47
)
48
49
// PollingServedPortsObserver regularly polls "/proc" to observe port changes.
50
type PollingServedPortsObserver struct {
51
RefreshInterval time.Duration
52
53
fileOpener func(fn string) (io.ReadCloser, error)
54
}
55
56
// Observe starts observing the served ports until the context is canceled.
57
func (p *PollingServedPortsObserver) Observe(ctx context.Context) (<-chan []ServedPort, <-chan error) {
58
if p.fileOpener == nil {
59
p.fileOpener = func(fn string) (io.ReadCloser, error) {
60
return os.Open(fn)
61
}
62
}
63
64
var (
65
errchan = make(chan error, 1)
66
reschan = make(chan []ServedPort)
67
ticker = time.NewTicker(p.RefreshInterval)
68
)
69
70
go func() {
71
defer close(errchan)
72
defer close(reschan)
73
74
for {
75
select {
76
case <-ctx.Done():
77
log.Info("Port observer stopped")
78
return
79
case <-ticker.C:
80
}
81
82
var (
83
visited = make(map[string]struct{})
84
ports []ServedPort
85
)
86
87
var protos []string
88
for _, path := range []string{fnNetTCP, fnNetTCP6} {
89
if _, err := os.Stat(path); err == nil {
90
protos = append(protos, path)
91
}
92
}
93
94
for _, fn := range protos {
95
fc, err := p.fileOpener(fn)
96
if err != nil {
97
errchan <- err
98
continue
99
}
100
ps, err := readNetTCPFile(fc, true)
101
fc.Close()
102
103
if err != nil {
104
errchan <- err
105
continue
106
}
107
for _, port := range ps {
108
key := fmt.Sprintf("%s:%d", hex.EncodeToString(port.Address), port.Port)
109
_, exists := visited[key]
110
if exists {
111
continue
112
}
113
visited[key] = struct{}{}
114
ports = append(ports, port)
115
}
116
}
117
118
if len(ports) > 0 {
119
reschan <- ports
120
}
121
}
122
}()
123
124
return reschan, errchan
125
}
126
127
func readNetTCPFile(fc io.Reader, listeningOnly bool) (ports []ServedPort, err error) {
128
scanner := bufio.NewScanner(fc)
129
for scanner.Scan() {
130
fields := strings.Fields(scanner.Text())
131
if len(fields) < 4 {
132
continue
133
}
134
if listeningOnly && fields[3] != "0A" {
135
continue
136
}
137
138
segs := strings.Split(fields[1], ":")
139
if len(segs) < 2 {
140
continue
141
}
142
addrHex, portHex := segs[0], segs[1]
143
144
port, err := strconv.ParseUint(portHex, 16, 32)
145
if err != nil {
146
log.WithError(err).WithField("port", portHex).Warn("cannot parse port entry from /proc/net/tcp* file")
147
continue
148
}
149
ipAddress := hexDecodeIP([]byte(addrHex))
150
151
ports = append(ports, ServedPort{
152
BoundToLocalhost: ipAddress.IsLoopback(),
153
Address: ipAddress,
154
Port: uint32(port),
155
})
156
157
sort.Slice(ports, func(i, j int) bool {
158
if ports[i].Address.Equal(ports[j].Address) {
159
return ports[i].Port < ports[j].Port
160
}
161
return bytes.Compare(ports[i].Address, ports[j].Address) < 0
162
})
163
164
sort.Slice(ports, func(i, j int) bool {
165
return ports[i].Port < ports[j].Port
166
})
167
}
168
if err = scanner.Err(); err != nil {
169
return nil, err
170
}
171
172
return
173
}
174
175
// Parses IPv4/IPv6 addresses. The address is a big endian 32 bit ints, hex encoded.
176
// We just decode the hex and flip the bytes in every group of 4.
177
func hexDecodeIP(src []byte) net.IP {
178
buf := make(net.IP, net.IPv6len)
179
180
blocks := len(src) / 8
181
for block := 0; block < blocks; block++ {
182
for i := 0; i < 4; i++ {
183
a := fromHexChar(src[block*8+i*2])
184
b := fromHexChar(src[block*8+i*2+1])
185
buf[block*4+3-i] = (a << 4) | b
186
}
187
}
188
return buf[:blocks*4]
189
}
190
191
// Converts a hex character into its value.
192
func fromHexChar(c byte) uint8 {
193
switch {
194
case '0' <= c && c <= '9':
195
return c - '0'
196
case 'a' <= c && c <= 'f':
197
return c - 'a' + 10
198
case 'A' <= c && c <= 'F':
199
return c - 'A' + 10
200
}
201
return 0
202
}
203
204