github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/network/portrange.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package network 5 6 import ( 7 "fmt" 8 "sort" 9 "strconv" 10 "strings" 11 12 "github.com/juju/errors" 13 ) 14 15 // PortRange represents a single range of ports. 16 type PortRange struct { 17 FromPort int 18 ToPort int 19 Protocol string 20 } 21 22 // IsValid determines if the port range is valid. 23 func (p PortRange) Validate() error { 24 proto := strings.ToLower(p.Protocol) 25 if proto != "tcp" && proto != "udp" { 26 return errors.Errorf(`invalid protocol %q, expected "tcp" or "udp"`, proto) 27 } 28 err := errors.Errorf( 29 "invalid port range %d-%d/%s", 30 p.FromPort, 31 p.ToPort, 32 p.Protocol, 33 ) 34 switch { 35 case p.FromPort > p.ToPort: 36 return err 37 case p.FromPort < 1 || p.FromPort > 65535: 38 return err 39 case p.ToPort < 1 || p.ToPort > 65535: 40 return err 41 } 42 return nil 43 } 44 45 // ConflictsWith determines if the two port ranges conflict. 46 func (a PortRange) ConflictsWith(b PortRange) bool { 47 if a.Protocol != b.Protocol { 48 return false 49 } 50 return a.ToPort >= b.FromPort && b.ToPort >= a.FromPort 51 } 52 53 func (p PortRange) String() string { 54 if p.FromPort == p.ToPort { 55 return fmt.Sprintf("%d/%s", p.FromPort, strings.ToLower(p.Protocol)) 56 } 57 return fmt.Sprintf("%d-%d/%s", p.FromPort, p.ToPort, strings.ToLower(p.Protocol)) 58 } 59 60 func (p PortRange) GoString() string { 61 return p.String() 62 } 63 64 type portRangeSlice []PortRange 65 66 func (p portRangeSlice) Len() int { return len(p) } 67 func (p portRangeSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 68 func (p portRangeSlice) Less(i, j int) bool { 69 p1 := p[i] 70 p2 := p[j] 71 if p1.Protocol != p2.Protocol { 72 return p1.Protocol < p2.Protocol 73 } 74 if p1.FromPort != p2.FromPort { 75 return p1.FromPort < p2.FromPort 76 } 77 return p1.ToPort < p2.ToPort 78 } 79 80 // SortPortRanges sorts the given ports, first by protocol, then by number. 81 func SortPortRanges(portRanges []PortRange) { 82 sort.Sort(portRangeSlice(portRanges)) 83 } 84 85 // CollapsePorts collapses a slice of ports into port ranges. 86 // 87 // NOTE(dimitern): This is deprecated and should be removed when 88 // possible. It still exists, because in a few places slices of Ports 89 // are converted to PortRanges internally. 90 func CollapsePorts(ports []Port) (result []PortRange) { 91 // First, convert ports to ranges, then sort them. 92 var portRanges []PortRange 93 for _, p := range ports { 94 portRanges = append(portRanges, PortRange{p.Number, p.Number, p.Protocol}) 95 } 96 SortPortRanges(portRanges) 97 fromPort := 0 98 toPort := 0 99 protocol := "" 100 // Now merge single port ranges while preserving the order. 101 for _, pr := range portRanges { 102 if fromPort == 0 { 103 // new port range 104 fromPort = pr.FromPort 105 toPort = pr.ToPort 106 protocol = pr.Protocol 107 } else if pr.FromPort == toPort+1 && protocol == pr.Protocol { 108 // continuing port range 109 toPort = pr.FromPort 110 } else { 111 // break in port range 112 result = append(result, 113 PortRange{ 114 Protocol: protocol, 115 FromPort: fromPort, 116 ToPort: toPort, 117 }) 118 fromPort = pr.FromPort 119 toPort = pr.ToPort 120 protocol = pr.Protocol 121 } 122 } 123 if fromPort != 0 { 124 result = append(result, PortRange{ 125 Protocol: protocol, 126 FromPort: fromPort, 127 ToPort: toPort, 128 }) 129 130 } 131 return 132 } 133 134 // ParsePortRange builds a PortRange from the provided string. If the 135 // string does not include a protocol then "tcp" is used. Validate() 136 // gets called on the result before returning. If validation fails the 137 // invalid PortRange is still returned. 138 // Example strings: "80/tcp", "443", "12345-12349/udp". 139 func ParsePortRange(inPortRange string) (PortRange, error) { 140 // Extract the protocol. 141 protocol := "tcp" 142 parts := strings.SplitN(inPortRange, "/", 2) 143 if len(parts) == 2 { 144 inPortRange = parts[0] 145 protocol = parts[1] 146 } 147 148 // Parse the ports. 149 portRange, err := parsePortRange(inPortRange) 150 if err != nil { 151 return portRange, errors.Trace(err) 152 } 153 portRange.Protocol = protocol 154 155 return portRange, portRange.Validate() 156 } 157 158 // MustParsePortRange converts a raw port-range string into a PortRange. 159 // If the string is invalid, the function panics. 160 func MustParsePortRange(portRange string) PortRange { 161 portrange, err := ParsePortRange(portRange) 162 if err != nil { 163 panic(err) 164 } 165 return portrange 166 } 167 168 func parsePortRange(portRange string) (PortRange, error) { 169 var result PortRange 170 var start, end int 171 parts := strings.Split(portRange, "-") 172 if len(parts) > 2 { 173 return result, errors.Errorf("invalid port range %q", portRange) 174 } 175 176 if len(parts) == 1 { 177 port, err := strconv.Atoi(parts[0]) 178 if err != nil { 179 return result, errors.Annotatef(err, "invalid port %q", portRange) 180 } 181 start = port 182 end = port 183 } else { 184 var err error 185 if start, err = strconv.Atoi(parts[0]); err != nil { 186 return result, errors.Annotatef(err, "invalid port %q", parts[0]) 187 } 188 if end, err = strconv.Atoi(parts[1]); err != nil { 189 return result, errors.Annotatef(err, "invalid port %q", parts[1]) 190 } 191 } 192 193 result = PortRange{ 194 FromPort: start, 195 ToPort: end, 196 } 197 return result, nil 198 } 199 200 // ParsePortRanges splits the provided string on commas and extracts a 201 // PortRange from each part of the split string. Whitespace is ignored. 202 // Example strings: "80/tcp", "80,443,1234/udp", "123-456, 25/tcp". 203 func ParsePortRanges(inPortRanges string) ([]PortRange, error) { 204 var portRanges []PortRange 205 for _, portRange := range strings.Split(inPortRanges, ",") { 206 portRange, err := ParsePortRange(strings.TrimSpace(portRange)) 207 if err != nil { 208 return portRanges, errors.Trace(err) 209 } 210 portRanges = append(portRanges, portRange) 211 } 212 return portRanges, nil 213 }