11package model
22
3- import "gorm.io/datatypes"
3+ import (
4+ "encoding/json"
5+ "fmt"
6+ "regexp"
7+ "strconv"
8+ "strings"
9+
10+ "gorm.io/datatypes"
11+ )
12+
13+ const (
14+ // maxPort is the largest TCP port that may be listened on.
15+ maxPort = 65535
16+
17+ // maxServerNameLength and maxLabelLength follow the DNS limits.
18+ maxServerNameLength = 253
19+ maxLabelLength = 63
20+ )
21+
22+ var (
23+ // portPattern matches the decimal notation of a TCP port.
24+ portPattern = regexp .MustCompile (`^[0-9]{1,5}$` )
25+
26+ // serverNameLabelPattern matches one label of a literal host name. The
27+ // accepted character set is deliberately narrow: the value ends up in an
28+ // nginx "server_name" directive, so anything that could terminate the
29+ // directive (or start another one) must not pass.
30+ serverNameLabelPattern = regexp .MustCompile (`^[A-Za-z0-9_]([A-Za-z0-9_-]*[A-Za-z0-9_])?$` )
31+ )
432
533type Website struct {
634 Base
@@ -14,3 +42,93 @@ type Website struct {
1442
1543 IsEnabled bool `gorm:"is_enabled;default=true" json:"is_enabled"`
1644}
45+
46+ // Validate checks the fields that tcontrollerd renders into the nginx
47+ // configuration of the site.
48+ //
49+ // Ports and server names are interpolated into nginx directives, so they have
50+ // to be plain ports and host names: a value carrying nginx syntax must be
51+ // rejected here, before it is stored and pushed to the engine.
52+ func (w * Website ) Validate () error {
53+ serverNames , err := unmarshalStringList (w .ServerNames )
54+ if err != nil {
55+ return fmt .Errorf ("invalid server_names: %v" , err )
56+ }
57+
58+ ports , err := unmarshalStringList (w .Ports )
59+ if err != nil {
60+ return fmt .Errorf ("invalid ports: %v" , err )
61+ }
62+
63+ for _ , serverName := range serverNames {
64+ if err = ValidateServerName (serverName ); err != nil {
65+ return err
66+ }
67+ }
68+
69+ for _ , port := range ports {
70+ if err = ValidatePort (port ); err != nil {
71+ return err
72+ }
73+ }
74+
75+ return nil
76+ }
77+
78+ // ValidatePort accepts the decimal notation of a TCP port.
79+ func ValidatePort (port string ) error {
80+ if ! portPattern .MatchString (port ) {
81+ return fmt .Errorf ("invalid port %q: only digits are allowed" , port )
82+ }
83+
84+ value , err := strconv .Atoi (port )
85+ if err != nil || value < 1 || value > maxPort {
86+ return fmt .Errorf ("invalid port %q: must be between 1 and %d" , port , maxPort )
87+ }
88+
89+ return nil
90+ }
91+
92+ // ValidateServerName accepts a literal host name, optionally with a wildcard
93+ // as its first label, or "*" for the default site.
94+ func ValidateServerName (name string ) error {
95+ // "*" (and the empty string, which the console sends for the same purpose)
96+ // makes the site the default server and is rendered as "_".
97+ if name == "*" || name == "" {
98+ return nil
99+ }
100+
101+ if len (name ) > maxServerNameLength {
102+ return fmt .Errorf ("invalid server name %q: longer than %d characters" , name , maxServerNameLength )
103+ }
104+
105+ for i , label := range strings .Split (name , "." ) {
106+ if label == "*" {
107+ // nginx only accepts a wildcard as the left most label.
108+ if i != 0 {
109+ return fmt .Errorf ("invalid server name %q: wildcard must be the first label" , name )
110+ }
111+ continue
112+ }
113+
114+ if len (label ) > maxLabelLength || ! serverNameLabelPattern .MatchString (label ) {
115+ return fmt .Errorf ("invalid server name %q: %q is not a valid host name label" , name , label )
116+ }
117+ }
118+
119+ return nil
120+ }
121+
122+ // unmarshalStringList decodes one of the JSON list columns of a website.
123+ func unmarshalStringList (raw datatypes.JSON ) ([]string , error ) {
124+ if len (raw ) == 0 {
125+ return nil , nil
126+ }
127+
128+ var values []string
129+ if err := json .Unmarshal (raw , & values ); err != nil {
130+ return nil , err
131+ }
132+
133+ return values , nil
134+ }
0 commit comments