github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/uniter/runner/jujuc/ports.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package jujuc
     5  
     6  import (
     7  	"fmt"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  
    16  	jujucmd "github.com/juju/juju/cmd"
    17  )
    18  
    19  const (
    20  	portFormat = "<port>[/<protocol>] or <from>-<to>[/<protocol>] or icmp"
    21  
    22  	portExp       = "(?:[0-9]+)"
    23  	protoExp      = "(?:[a-z0-9]+)"
    24  	portPlusProto = portExp + "(?:-" + portExp + ")?(/" + protoExp + ")?"
    25  )
    26  
    27  var validPortOrRange = regexp.MustCompile("^icmp|" + portPlusProto + "$")
    28  
    29  type port struct {
    30  	number   int
    31  	protocol string
    32  }
    33  
    34  func (p port) validate() error {
    35  	proto := strings.ToLower(p.protocol)
    36  	if proto == "icmp" {
    37  		if p.number == -1 {
    38  			return nil
    39  		}
    40  		return errors.Errorf(`protocol "icmp" doesn't support any ports; got "%v"`, p.number)
    41  	}
    42  	if p.number < 1 || p.number > 65535 {
    43  		return errors.Errorf(`port must be in the range [1, 65535]; got "%v"`, p.number)
    44  	}
    45  	if proto != "tcp" && proto != "udp" && proto != "icmp" {
    46  		return errors.Errorf(`protocol must be "tcp", "udp", or "icmp"; got %q`, p.protocol)
    47  	}
    48  	return nil
    49  }
    50  
    51  type portRange struct {
    52  	fromPort, toPort int
    53  	protocol         string
    54  }
    55  
    56  func (pr portRange) validate() error {
    57  	if pr.fromPort == pr.toPort {
    58  		return port{pr.fromPort, pr.protocol}.validate()
    59  	}
    60  	if pr.fromPort > pr.toPort {
    61  		return errors.Errorf(
    62  			"invalid port range %d-%d/%s; expected fromPort <= toPort",
    63  			pr.fromPort, pr.toPort, pr.protocol,
    64  		)
    65  	}
    66  	proto := strings.ToLower(pr.protocol)
    67  	if proto == "icmp" {
    68  		if pr.fromPort == pr.toPort && pr.fromPort == -1 {
    69  			return nil
    70  		}
    71  		return errors.Errorf(`protocol "icmp" doesn't support any ports; got "%v"`, pr.fromPort)
    72  	}
    73  	if pr.fromPort < 1 || pr.fromPort > 65535 {
    74  		return errors.Errorf(`fromPort must be in the range [1, 65535]; got "%v"`, pr.fromPort)
    75  	}
    76  	if pr.toPort < 1 || pr.toPort > 65535 {
    77  		return errors.Errorf(`toPort must be in the range [1, 65535]; got "%v"`, pr.toPort)
    78  	}
    79  	if proto != "tcp" && proto != "udp" && proto != "icmp" {
    80  		return errors.Errorf(`protocol must be "tcp", "udp", or "icmp"; got %q`, pr.protocol)
    81  	}
    82  	return nil
    83  }
    84  
    85  func parseArguments(args []string) (portRange, error) {
    86  	arg := strings.ToLower(args[0])
    87  	if !validPortOrRange.MatchString(arg) {
    88  		return portRange{}, errors.Errorf("expected %s; got %q", portFormat, args[0])
    89  	}
    90  	portOrRange := validPortOrRange.FindString(arg)
    91  	parts := strings.SplitN(portOrRange, "/", 2)
    92  
    93  	protocol := "tcp"
    94  	if len(parts) > 1 {
    95  		protocol = parts[1]
    96  	}
    97  	ports := parts[0]
    98  	portParts := strings.SplitN(ports, "-", 2)
    99  	fromPort, toPort := 0, 0
   100  	if len(portParts) >= 1 {
   101  		if portParts[0] == "icmp" {
   102  			protocol = "icmp"
   103  			fromPort, toPort = -1, -1
   104  		} else {
   105  			port, err := strconv.Atoi(portParts[0])
   106  			if err != nil {
   107  				return portRange{}, errors.Annotatef(err, "expected port number; got %q", portParts[0])
   108  			}
   109  			fromPort = port
   110  		}
   111  	}
   112  	if len(portParts) == 2 {
   113  		port, err := strconv.Atoi(portParts[1])
   114  		if err != nil {
   115  			return portRange{}, errors.Annotatef(err, "expected port number; got %q", portParts[1])
   116  		}
   117  		toPort = port
   118  	} else {
   119  		toPort = fromPort
   120  	}
   121  	pr := portRange{fromPort, toPort, protocol}
   122  	return pr, pr.validate()
   123  }
   124  
   125  // portCommand implements the open-port and close-port commands.
   126  type portCommand struct {
   127  	cmd.CommandBase
   128  	info       *cmd.Info
   129  	action     func(*portCommand) error
   130  	Protocol   string
   131  	FromPort   int
   132  	ToPort     int
   133  	formatFlag string // deprecated
   134  }
   135  
   136  func (c *portCommand) Info() *cmd.Info {
   137  	return jujucmd.Info(c.info)
   138  }
   139  
   140  func (c *portCommand) SetFlags(f *gnuflag.FlagSet) {
   141  	f.StringVar(&c.formatFlag, "format", "", "deprecated format flag")
   142  }
   143  
   144  func (c *portCommand) Init(args []string) error {
   145  	if args == nil {
   146  		return errors.Errorf("no port or range specified")
   147  	}
   148  
   149  	portRange, err := parseArguments(args)
   150  	if err != nil {
   151  		return errors.Trace(err)
   152  	}
   153  
   154  	c.FromPort = portRange.fromPort
   155  	c.ToPort = portRange.toPort
   156  	c.Protocol = portRange.protocol
   157  	return cmd.CheckEmpty(args[1:])
   158  }
   159  
   160  func (c *portCommand) Run(ctx *cmd.Context) error {
   161  	if c.formatFlag != "" {
   162  		fmt.Fprintf(ctx.Stderr, "--format flag deprecated for command %q", c.Info().Name)
   163  	}
   164  	return c.action(c)
   165  }
   166  
   167  var openPortInfo = &cmd.Info{
   168  	Name:    "open-port",
   169  	Args:    portFormat,
   170  	Purpose: "register a port or range to open",
   171  	Doc:     "The port range will only be open while the application is exposed.",
   172  }
   173  
   174  func NewOpenPortCommand(ctx Context) (cmd.Command, error) {
   175  	return &portCommand{
   176  		info: openPortInfo,
   177  		action: func(c *portCommand) error {
   178  			return ctx.OpenPorts(c.Protocol, c.FromPort, c.ToPort)
   179  		},
   180  	}, nil
   181  }
   182  
   183  var closePortInfo = &cmd.Info{
   184  	Name:    "close-port",
   185  	Args:    portFormat,
   186  	Purpose: "ensure a port or range is always closed",
   187  }
   188  
   189  func NewClosePortCommand(ctx Context) (cmd.Command, error) {
   190  	return &portCommand{
   191  		info: closePortInfo,
   192  		action: func(c *portCommand) error {
   193  			return ctx.ClosePorts(c.Protocol, c.FromPort, c.ToPort)
   194  		},
   195  	}, nil
   196  }