Fine tuning
This commit is contained in:
		
							parent
							
								
									797e4640df
								
							
						
					
					
						commit
						c684a39191
					
				
					 4 changed files with 35 additions and 26 deletions
				
			
		| 
						 | 
					@ -38,7 +38,7 @@ var flagsServe = []cli.Flag{
 | 
				
			||||||
	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
 | 
						altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
 | 
				
			||||||
	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
 | 
						altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
 | 
				
			||||||
	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
 | 
						altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
 | 
				
			||||||
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}),
 | 
						altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
 | 
				
			||||||
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
 | 
						altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
 | 
				
			||||||
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
 | 
						altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
 | 
				
			||||||
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
 | 
						altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
 | 
				
			||||||
| 
						 | 
					@ -143,7 +143,7 @@ func execServe(c *cli.Context) error {
 | 
				
			||||||
		return errors.New("if set, base-url must start with http:// or https://")
 | 
							return errors.New("if set, base-url must start with http:// or https://")
 | 
				
			||||||
	} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
 | 
						} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
 | 
				
			||||||
		return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
 | 
							return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
 | 
				
			||||||
	} else if !util.InStringList([]string{"app", "home"}, webRoot) {
 | 
						} else if !util.InStringList([]string{"app", "home", "disable"}, webRoot) {
 | 
				
			||||||
		return errors.New("if set, web-root must be 'home' or 'app'")
 | 
							return errors.New("if set, web-root must be 'home' or 'app'")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -802,7 +802,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
 | 
				
			||||||
| `smtp-server-addr-prefix`                  | `NTFY_SMTP_SERVER_ADDR_PREFIX`                  | `[ip]:port`                                         | -            | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-`                                                                                                                                                          |
 | 
					| `smtp-server-addr-prefix`                  | `NTFY_SMTP_SERVER_ADDR_PREFIX`                  | `[ip]:port`                                         | -            | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-`                                                                                                                                                          |
 | 
				
			||||||
| `keepalive-interval`                       | `NTFY_KEEPALIVE_INTERVAL`                       | *duration*                                          | 45s          | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
 | 
					| `keepalive-interval`                       | `NTFY_KEEPALIVE_INTERVAL`                       | *duration*                                          | 45s          | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
 | 
				
			||||||
| `manager-interval`                         | `$NTFY_MANAGER_INTERVAL`                        | *duration*                                          | 1m           | Interval in which the manager prunes old messages, deletes topics and prints the stats.                                                                                                                                         |
 | 
					| `manager-interval`                         | `$NTFY_MANAGER_INTERVAL`                        | *duration*                                          | 1m           | Interval in which the manager prunes old messages, deletes topics and prints the stats.                                                                                                                                         |
 | 
				
			||||||
| `web-root`                                 | `NTFY_WEB_ROOT`                                 | `app` or `home`                                     | `app`        | Sets web root to landing page (home), web app (app) or (disable) for no WebUI.                                                                                                                                                  |
 | 
					| `web-root`                                 | `NTFY_WEB_ROOT`                                 | `app`, `home` or `disable`                          | `app`        | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable)                                                                                                                                  |
 | 
				
			||||||
| `global-topic-limit`                       | `NTFY_GLOBAL_TOPIC_LIMIT`                       | *number*                                            | 15,000       | Rate limiting: Total number of topics before the server rejects new topics.                                                                                                                                                     |
 | 
					| `global-topic-limit`                       | `NTFY_GLOBAL_TOPIC_LIMIT`                       | *number*                                            | 15,000       | Rate limiting: Total number of topics before the server rejects new topics.                                                                                                                                                     |
 | 
				
			||||||
| `visitor-subscription-limit`               | `NTFY_VISITOR_SUBSCRIPTION_LIMIT`               | *number*                                            | 30           | Rate limiting: Number of subscriptions per visitor (IP address)                                                                                                                                                                 |
 | 
					| `visitor-subscription-limit`               | `NTFY_VISITOR_SUBSCRIPTION_LIMIT`               | *number*                                            | 30           | Rate limiting: Number of subscriptions per visitor (IP address)                                                                                                                                                                 |
 | 
				
			||||||
| `visitor-attachment-total-size-limit`      | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT`      | *size*                                              | 100M         | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`.                                                 |
 | 
					| `visitor-attachment-total-size-limit`      | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT`      | *size*                                              | 100M         | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`.                                                 |
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -263,24 +263,24 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
					func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
	if r.Method == http.MethodGet && r.URL.Path == "/" && s.config.EnableWeb {
 | 
						if r.Method == http.MethodGet && r.URL.Path == "/" {
 | 
				
			||||||
		return s.handleHome(w, r)
 | 
							return s.ensureWebEnabled(s.handleHome)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" && s.config.EnableWeb {
 | 
						} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
 | 
				
			||||||
		return s.handleExample(w, r)
 | 
							return s.ensureWebEnabled(s.handleExample)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodHead && r.URL.Path == "/" {
 | 
						} else if r.Method == http.MethodHead && r.URL.Path == "/" {
 | 
				
			||||||
		return s.handleEmpty(w, r, v)
 | 
							return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath && s.config.EnableWeb {
 | 
						} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
 | 
				
			||||||
		return s.handleWebConfig(w, r)
 | 
							return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
 | 
						} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
 | 
				
			||||||
		return s.handleUserStats(w, r, v)
 | 
							return s.handleUserStats(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) && s.config.EnableWeb {
 | 
						} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
 | 
				
			||||||
		return s.handleStatic(w, r)
 | 
							return s.ensureWebEnabled(s.handleStatic)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) && s.config.EnableWeb {
 | 
						} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
 | 
				
			||||||
		return s.handleDocs(w, r)
 | 
							return s.ensureWebEnabled(s.handleDocs)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
 | 
						} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
 | 
				
			||||||
		return s.limitRequests(s.handleFile)(w, r, v)
 | 
							return s.limitRequests(s.handleFile)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodOptions {
 | 
						} else if r.Method == http.MethodOptions {
 | 
				
			||||||
		return s.handleOptions(w, r)
 | 
							return s.ensureWebEnabled(s.handleOptions)(w, r, v)
 | 
				
			||||||
	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" {
 | 
						} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" {
 | 
				
			||||||
		return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v)
 | 
							return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v)
 | 
				
			||||||
	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
 | 
						} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
 | 
				
			||||||
| 
						 | 
					@ -298,21 +298,21 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 | 
				
			||||||
	} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
 | 
						} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
 | 
				
			||||||
		return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
 | 
							return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) {
 | 
						} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) {
 | 
				
			||||||
		return s.handleTopic(w, r)
 | 
							return s.ensureWebEnabled(s.handleTopic)(w, r, v)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return errHTTPNotFound
 | 
						return errHTTPNotFound
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
 | 
					func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
	if s.config.WebRootIsApp {
 | 
						if s.config.WebRootIsApp {
 | 
				
			||||||
		r.URL.Path = webAppIndex
 | 
							r.URL.Path = webAppIndex
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		r.URL.Path = webHomeIndex
 | 
							r.URL.Path = webHomeIndex
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return s.handleStatic(w, r)
 | 
						return s.handleStatic(w, r, v)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
 | 
					func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
	unifiedpush := readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see PUT/POST too!
 | 
						unifiedpush := readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see PUT/POST too!
 | 
				
			||||||
	if unifiedpush {
 | 
						if unifiedpush {
 | 
				
			||||||
		w.Header().Set("Content-Type", "application/json")
 | 
							w.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
| 
						 | 
					@ -321,7 +321,7 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	r.URL.Path = webAppIndex
 | 
						r.URL.Path = webAppIndex
 | 
				
			||||||
	return s.handleStatic(w, r)
 | 
						return s.handleStatic(w, r, v)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
 | 
					func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
 | 
				
			||||||
| 
						 | 
					@ -335,12 +335,12 @@ func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Request, _ *visi
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error {
 | 
					func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
 | 
				
			||||||
	_, err := io.WriteString(w, exampleSource)
 | 
						_, err := io.WriteString(w, exampleSource)
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleWebConfig(w http.ResponseWriter, r *http.Request) error {
 | 
					func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
 | 
				
			||||||
	appRoot := "/"
 | 
						appRoot := "/"
 | 
				
			||||||
	if !s.config.WebRootIsApp {
 | 
						if !s.config.WebRootIsApp {
 | 
				
			||||||
		appRoot = "/app"
 | 
							appRoot = "/app"
 | 
				
			||||||
| 
						 | 
					@ -368,13 +368,13 @@ func (s *Server) handleUserStats(w http.ResponseWriter, r *http.Request, v *visi
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
 | 
					func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
 | 
				
			||||||
	r.URL.Path = webSiteDir + r.URL.Path
 | 
						r.URL.Path = webSiteDir + r.URL.Path
 | 
				
			||||||
	util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
 | 
						util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
 | 
					func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ *visitor) error {
 | 
				
			||||||
	util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r)
 | 
						util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r)
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -905,7 +905,7 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
 | 
				
			||||||
	return sinceNoMessages, errHTTPBadRequestSinceInvalid
 | 
						return sinceNoMessages, errHTTPBadRequestSinceInvalid
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
 | 
					func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
 | 
				
			||||||
	w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
 | 
						w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
 | 
				
			||||||
	w.Header().Set("Access-Control-Allow-Origin", "*")  // CORS, allow cross-origin requests
 | 
						w.Header().Set("Access-Control-Allow-Origin", "*")  // CORS, allow cross-origin requests
 | 
				
			||||||
	w.Header().Set("Access-Control-Allow-Headers", "*") // CORS, allow auth via JS // FIXME is this terrible?
 | 
						w.Header().Set("Access-Control-Allow-Headers", "*") // CORS, allow auth via JS // FIXME is this terrible?
 | 
				
			||||||
| 
						 | 
					@ -1119,6 +1119,15 @@ func (s *Server) limitRequests(next handleFunc) handleFunc {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
 | 
							if !s.config.EnableWeb {
 | 
				
			||||||
 | 
								return errHTTPNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return next(w, r, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// transformBodyJSON peeks the request body, reads the JSON, and converts it to headers
 | 
					// transformBodyJSON peeks the request body, reads the JSON, and converts it to headers
 | 
				
			||||||
// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
 | 
					// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
 | 
				
			||||||
func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
 | 
					func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -128,7 +128,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
 | 
					# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
 | 
				
			||||||
# web app. If you self-host, you don't want to change this.
 | 
					# web app. If you self-host, you don't want to change this.
 | 
				
			||||||
# Can be "app" (default), "home" or "disable" to disable the WebUI.
 | 
					# Can be "app" (default), "home" or "disable" to disable the web app entirely.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# web-root: app
 | 
					# web-root: app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue