server

package module
v3.0.0-beta.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 24, 2024 License: MIT Imports: 21 Imported by: 1

README

Nix Server (v2)

Overview

This package provides an HTTP/S server that can be easily configured for serving static files in few lines of code, but also scaled to have a server that can handle multiple domains and subdomains, with their logic for every incoming request, backgound tasks and event logging.


Package structure

The package mainly relies on the Server object, which listens on a specific port and forwards every connection to the inner handler.

Basic Implementation

For a basic Server configuration that serves every connection with the content of the public folder located in the working directory, here is how to configure it:

package main

import (
	"os"
	"os/signal"

	"github.com/nixpare/server/v2"
)

func main() {
	// Create a new server on port 8080, not secure
	// (that means using http and not https) and empty
	// path (so the path is the working directory)
	srv, err := server.NewServer(8080, false, "")
	if err != nil {
		panic(err)
	}

	// Register a default route that serves every files inside
	// the /public folder
	srv.RegisterDefaultRoute("Default", server.SubdomainConfig{})
	// The server starts listening on the port
	srv.Start()

	// Listens for a Control-C
	exitC := make(chan os.Signal)
	signal.Notify(exitC, os.Interrupt)
	<- exitC
	// Stops the server after the Control-C
	srv.Stop()
}
Advanced Implementation

For a more anvanced implementation we need a Router that will manage every server listening on different ports and domains, along with a TaskManager that can be used for creating panic-safe goroutines running in the background.

So we first need a router

// new router with path set to the working directory and log output
// equal to the stdout
router, _ := server.NewRouter("", nil)

that will be used to create the servers (we will only implement one)

srv, _ := router.NewServer(443, true, server.Certificate{
	CertPemPath: "path/to/fullchain_key.pem",
	KeyPemPath: "path/to/private_key.pem",
}/*, other certificates concatenated ...*/)

then on each server we can register multiple domains and subdmains

mainDomain := srv.RegisterDomain("My Domain", "mydomain.com")
localDomain := srv.RegisterDomain("Localhost Connections", "localhost")

// This registers the subdomain sub.mydomain.com
// serveFunction will be explained below
mainDomain.RegisterSubdomain("sub", server.SubdomainConfig{
	ServeF: serveFunction,
	Website: server.Website{
		Name: "My Website",
		Dir: "Custom Dir holding files",
		PageHeaders: map[string][][2]string{
			"/": {
				{"header_name_1", "value for index page"},
				{"header_name_2", "other value for index page"}
			},
			"/page": {{"header_name", "value for page page"}},
		} // The PageHeaders field can be seen as an object that maps
		  // a string to a series of string couples, each rapresenting
		  // the key-value pair for an http header
	}
})

localDomain.RegisterSubdomain(...)

The serving function - Route

To manage every connection with your own logic, the only structures you need are the Route and Website ones, and in order to let the server know which function to call you have to provide one in the ServeF field of the SubdomainConfig. The function must have a signature as descrived by the type server.ServeFunction -> func(route *server.Route), that is a simple function taking in input just the Route. This structure holds everything you need for a web request:

  • functions to serve content (ServeFile, ServeData, Error)
  • functions to manage cookies (SetCookie, DeleteCookie, DecodeCookie)
  • other functions
  • reference to the underlying http.ResponseWriter and *http.Request for using any standard http function from the stardard library (or even third party ones that need those type of structures)

The TaskManager

It can used to manage background functions and processes running in background, with panic protection to avoid the crash of the entire program and the possibility to listen for the router shutdown in order to interrupt any sensitive procedure. This functions so have a lifecycle composed of:

  • a startup function that is ran when the task is started for the first time (unless it's later stopped)
  • a cleaup function that is run when the task is stopped
  • an exec function that can be ran manually or by the task timer

The task timer determines if and how ofter the task exec function should be called and can be changed at any time after creation

Other utility functions

Inside the utility.go file of the package there are some useful functions, but mostly I highlight these two of them:

  • RandStr, which generates a random string with the given length populated with the chosen sets of characters (see CharSet constants)
  • PanicToErr, which can be used to call any function returning an error and automatically wrapping them in a panic-safe environment that converts the panic into a simple error with a helpful stack trace for the panic (or just returns the simple error of the function)
func myFunction(arg string) error {
  if arg == "" {
  	panic("Empty arg")
  }
  return errors.New(arg)
}
myArg = "argument"
err := PanicToErr(func() error { return myFunction(myArg) })
// err.Error() is not nil both when the function returned an error
// or a panic has occurred, but:
//  - in the first case, only the err.Err field will be set
//  - in the second case, both the err.PanicErr and err.Stack will be set
if err.Error() != nil {
  // ...
}

Documentation

Overview

Package server provides an HTTP/S server that can be easily configured for serving static files in few lines of code, but also scaled to have a server that can handle multiple domains and subdomains, with their logic for every incoming request, backgound tasks and event logging.

Package structure

The package mainly relies on the Server object, which listens on a specific port and forwards every connection to the inner handler.

Basic Implementation:

For a basic `Server` configuration that serves every connection with the content of the `public` folder located in the working directory, here is how to configure it:

package main

import (
  "os"
  "os/signal"

  "github.com/nixpare/server/v2"
)

func main() {
  // Create a new server on port 8080, not secure
  // (that means using http and not https) and empty
  // path (so the path is the working directory)
  srv, err := server.NewServer(8080, false, "")
  if err != nil {
  	panic(err)
  }

  // Register a default route that serves every files inside
  // the /public folder
  srv.RegisterDefaultRoute("Default", server.SubdomainConfig{})
  // The server starts listening on the port
  srv.Start()

  // Listens for a Control-C
  exitC := make(chan os.Signal)
  signal.Notify(exitC, os.Interrupt)
  <- exitC
  // Stops the server after the Control-C
  srv.Stop()
}

Advanced Implementation:

For a more anvanced implementation we need a `Router` that will manage every server listening on different ports and domains, along with a `TaskManager` that can be used for creating panic-safe goroutines running in the background.

So we first need a router

// new router with path set to the working directory and log output
// equal to the stdout
router, _ := server.NewRouter("", nil)

that will be used to create the servers (we will only implement one)

srv, _ := router.NewServer(443, true, server.Certificate{
  CertPemPath: "path/to/fullchain_key.pem",
  KeyPemPath: "path/to/private_key.pem",
}, // other certificates concatenated ...)

then on each server we can register multiple domains and subdmains

mainDomain := srv.RegisterDomain("My Domain", "mydomain.com")
localDomain := srv.RegisterDomain("Localhost Connections", "localhost")
// This registers the subdomain sub.mydomain.com
// serveFunction will be explained below
mainDomain.RegisterSubdomain("sub", server.SubdomainConfig{
  ServeF: serveFunction,
  Website: server.Website{
  	 Name: "My Website",
  	 Dir: "Custom Dir holding files",
  	 PageHeaders: map[string][][2]string{
  	   "/": {
  	   	 {"header_name_1", "value for index page"},
  	   	 {"header_name_2", "other value for index page"}
  	   },
  	   "/page": {{"header_name", "value for page page"}},
  	 } // The PageHeaders field can be seen as an object that maps
  	   // a string to a series of string couples, each rapresenting
  	   // the key-value pair for an http header
  }
})
localDomain.RegisterSubdomain(...)

The serving function - Route

To manage every connection with your own logic, the only structures you need are the `Route` and `Website` ones, and in order to let the server know which function to call you have to provide one in the `ServeF` field of the `SubdomainConfig`. The function must have a signature as descrived by the type `server.ServeFunction -> func(route *server.Route)`, that is a simple function taking in input just the Route. This structure holds everything you need for a web request:

  • functions to serve content (`ServeFile`, `ServeData`, `Error`)
  • functions to manage cookies (`SetCookie`, `DeleteCookie`, `DecodeCookie`)
  • other functions
  • reference to the underlying `http.ResponseWriter` and `*http.Request` for using any standard http function from the stardard library (or even third party ones that need those type of structures)

The TaskManager

It can used to manage background function running in background, with panic protection to avoid the crash of the entire program and the possibility to listen for the router shutdown in order to interrupt any sensitive procedure. This functions so have a lifecycle composed of:

  • a startup function that is ran when the task is started for the first time (unless it's later stopped)
  • a cleaup function that is run when the task is stopped
  • an exec function that can be ran manually or by the task timer

The task timer determines if and how ofter the task exec function should be called and can be changed at any time after creation

Other utility functions

Inside the `utility.go` file of the package there are some useful functions, but mostly I highlight these two of them:

  • `RandStr`, which generates a random string with the given length populated with the chosen sets of characters (see `CharSet` constants)
  • `PanicToErr`, which can be used to call any function returning an error and automatically wrapping them in a panic-safe environment that converts the panic into a simple error with a helpful stack trace for the panic (or just returns the simple error of the function)

Here is a usage example:

func myFunction(arg string) error {
  if arg == "" {
    panic("Empty arg")
  }
  return errors.New(arg)
}
myArg = "argument"
err := PanicToErr(func() error { return myFunction(myArg) })
// err.Error() is not nil both when the function returned an error
// or a panic has occurred, but:
//  - in the first case, only the err.Err field will be set
//  - in the second case, both the err.PanicErr and err.Stack will be set
if err.Error() != nil {
  // Handle the error
}

Index

Constants

View Source
const (
	// TASK_TIMER_10_SECONDS determines a Task execution interval of 10 seconds
	TASK_TIMER_10_SECONDS = TaskTimer(time.Second / 1000000000 * 10)
	// TASK_TIMER_1_MINUTE determines a Task execution interval of 1 minute
	TASK_TIMER_1_MINUTE = TaskTimer(time.Minute / 1000000000 * 1)
	// TASK_TIMER_10_MINUTES determines a Task execution interval of 10 minutes
	TASK_TIMER_10_MINUTES = TaskTimer(time.Minute / 1000000000 * 10)
	// TASK_TIMER_30_MINUTES determines a Task execution interval of 30 minutes
	TASK_TIMER_30_MINUTES = TaskTimer(time.Minute / 1000000000 * 30)
	// TASK_TIMER_1_HOUR determines a Task execution interval of 1 hour
	TASK_TIMER_1_HOUR = TaskTimer(time.Hour / 1000000000)
	// TASK_TIMER_INACTIVE deactivates the Task automatic execution
	TASK_TIMER_INACTIVE = -1
)

Variables

View Source
var (
	ErrNotFound          = errors.New("not found")
	ErrAlreadyRegistered = errors.New("already registered")
)
View Source
var (
	TLSDefaultCipherSuiteIDs   []uint16
	TLSDefaultCurvePreferences []tls.CurveID
)

Used for creating a secure HTTP Server. See the tls.Config struct in the standard library for more informations

View Source
var (
	TimeFormat = "2006-01-02 15:04:05.00" // TimeFormat defines which timestamp to use with the logs. It can be modified.
)

Functions

func GenerateTSLConfig

func GenerateTSLConfig(certs []Certificate) (*tls.Config, error)

func PrepSubdomainName

func PrepSubdomainName(name string) string

PrepSubdomainName sanitizes the subdomain name

func RandStr

func RandStr(length int, randType CharSet) string

RandStr generates a random string with the given length. The string can be made of differente sets of characters: see CharSet type

func SplitAddrPort

func SplitAddrPort(host string) string

func TCPPipe

func TCPPipe(conn1, conn2 net.Conn)

Types

type Certificate

type Certificate struct {
	FullChainCert string // CertPemPath is the path to the full chain public key
	PrivateKey    string // KeyPemPath is the path to the private key
}

Certificate rapresents a standard PEM certicate composed of a full chain public key and a private key. This is used when creating an HTTPS server

type CharSet

type CharSet int

CharSet groups the possible output of the function RandStr. For the possible values see the constants

const (
	NUM               CharSet = iota // Digits from 0 to 9
	ALPHA                            // Latin letters from A to z (Uppercase and Lowercase)
	ALPHA_LOW                        // Latin letters from a to z (Lowercase)
	ALPHA_NUM                        // Combination of NUM and ALPHA
	ALPHA_LOW_NUM                    // Combination of NUM and ALPHA_LOW
	ALPHA_NUM_SPECIAL                // Combines ALPHA_LOW with this special character: !?+*-_=.&%$€#@
)

type ConnHandlerFunc

type ConnHandlerFunc func(srv *TCPServer, conn net.Conn)

func TCPProxy

func TCPProxy(address string, port int) (ConnHandlerFunc, error)

type HTTPServer

type HTTPServer struct {

	// Server is the underlying HTTP server from the standard library
	Server *http.Server
	// HTTP3Server is the QUIC Server, if is nil if the Server is not secure
	HTTP3Server *http3.Server

	Logger *logger.Logger

	Handler http.Handler

	Online     bool
	OnlineTime time.Time
	// contains filtered or unexported fields
}

HTTPServer is a single HTTP server listening on a TCP port. It can handle multiple domains and subdomains. To manage multiple servers listening on different ports use a Router.

Before creating any server you should change the HashKeyString and BlockKeyString global variables: see Route.SetCookiePerm method

func NewHTTPServer

func NewHTTPServer(address string, port int, certs ...Certificate) (*HTTPServer, error)

NewServer creates a new server

func (*HTTPServer) IsRunning

func (srv *HTTPServer) IsRunning() bool

IsRunning tells whether the server is running or not

func (*HTTPServer) Port

func (srv *HTTPServer) Port() int

Port returns the TCP port listened by the server

func (*HTTPServer) Router

func (srv *HTTPServer) Router() *Router

func (*HTTPServer) ServeHTTP

func (srv *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*HTTPServer) Start

func (srv *HTTPServer) Start() error

Start prepares every domain and subdomain and starts listening on the TCP port

func (*HTTPServer) Stop

func (srv *HTTPServer) Stop() error

Stop cleans up every domain and subdomain and stops listening on the TCP port

type Router

type Router struct {
	TaskManager *TaskManager
	Logger      *logger.Logger
	// contains filtered or unexported fields
}

Router is the main element of this package and is used to manage all the servers and the background tasks.

func NewRouter

func NewRouter(l *logger.Logger) *Router

NewRouter returns a new Router ready to be set up. If routerPath is not provided, the router will try to get the working directory; if logger is nil, the standard logger.DefaultLogger will be used

func (*Router) CustomServer

func (router *Router) CustomServer(port int) Server

Server returns the TCP server running on the given port

func (*Router) HTTPServer

func (router *Router) HTTPServer(port int) *HTTPServer

Server returns the HTTP server running on the given port

func (*Router) IsRunning

func (router *Router) IsRunning() bool

func (*Router) NewHTTPServer

func (router *Router) NewHTTPServer(address string, port int, certs ...Certificate) (*HTTPServer, error)

NewServer creates a new HTTP/HTTPS Server linked to the Router. See NewServer function for more information

func (*Router) NewTCPServer

func (router *Router) NewTCPServer(address string, port int, secure bool, certs ...Certificate) (*TCPServer, error)

NewServer creates a new TCP Server linked to the Router. See NewTCPServer function for more information

func (*Router) RegisterCustomServer

func (router *Router) RegisterCustomServer(srv Server) error

NewServer creates a new TCP Server linked to the Router. See NewTCPServer function for more information

func (*Router) Start

func (router *Router) Start()

Start starts all the registered servers and the background task manager

func (*Router) StartTime

func (router *Router) StartTime() time.Time

func (*Router) Stop

func (router *Router) Stop()

Stop starts the shutdown procedure of the entire router with all the servers registered, the background programs and tasks and lastly executes the router.CleanupF function, if set

func (*Router) TCPServer

func (router *Router) TCPServer(port int) *TCPServer

Server returns the TCP server running on the given port

type Server

type Server interface {
	Start() error
	Stop() error
	Port() int
	Secure() bool
	IsRunning() bool
}

type ServerRouter

type ServerRouter interface {
	Router() *Router
}

type TCPServer

type TCPServer struct {
	Online bool

	ConnHandler ConnHandlerFunc
	Router      *Router
	Logger      *logger.Logger
	// contains filtered or unexported fields
}

func NewTCPServer

func NewTCPServer(address string, port int, secure bool, certs ...Certificate) (*TCPServer, error)

func (*TCPServer) Address

func (srv *TCPServer) Address() string

func (*TCPServer) IsSecure

func (srv *TCPServer) IsSecure() bool

func (*TCPServer) Port

func (srv *TCPServer) Port() int

func (*TCPServer) Start

func (srv *TCPServer) Start()

func (*TCPServer) Stop

func (srv *TCPServer) Stop() error

type Task

type Task struct {
	InitF    TaskFunc  // InitF is the function called when the Task is started
	ExecF    TaskFunc  // ExecF is the function called every time the Task must be executed (from the timer or manually)
	CleanupF TaskFunc  // CleanupF is the function called when the Task is removed from the TaskManager or when the TaskManager is stopped (e.g. on Router shutdown)
	Timer    TaskTimer // TaskTimer is the Task execution interval, that is how often the function ExecF is called

	TaskManager *TaskManager
	Logger      *logger.Logger
	// contains filtered or unexported fields
}

Task is composed of a name set upon creation and of 3 functions necessary of the correct execution of a kind of program. Every function is panic-protected, this means that the entire server will not crash when some parts of the task fails badly; this does not mean that you can't handle panics by yourself, but if they are not handled its like catching them and returning their message as an error. If a function returns an error, this will be logged, providing the task name and which function called it automatically (the task will be disabled if the initialization fails, but you can do it manually, see router.SetBackgroundTaskState)

func (*Task) Cleaup

func (t *Task) Cleaup() error

stopTask runs the cleanup function, catching every possible error or panic

func (*Task) Exec

func (t *Task) Exec() error

Exec runs the initialization function, catching every possible error or panic, and then sets the flag Task.initDone to true. If the function fails it deactivates the task

func (*Task) Init

func (t *Task) Init() error

Init runs the initialization function, catching every possible error or panic, and then sets the flag Task.initDone to true. If the function fails it deactivates the task

func (*Task) IsReady

func (t *Task) IsReady() bool

func (*Task) IsRunning

func (t *Task) IsRunning() bool

func (*Task) ListenForExit

func (t *Task) ListenForExit() bool

ListenForExit waits until the exit signal is received from the manager. This signal is sent when you manually stop a task or the server is shutting down: in the last case the manager will wait for a maximum of 10 seconds, after those, if the execution is not finished, it will first kill the task and then call the cleanup function. This function is intended to be called in a goroutine listening for the signal: considering that the goroutine could stay alive even after the task exec function has exited, if this function returns true, this means that the signal is received correctly for that execution and you should exit, otherwise this means that the execution of the task has already terminated and thus you should not do anything Example:

execF = func(tm *server.TaskManager, t *server.Task) error {
	go func () {
		if !t.ListenForExit() {
			return 		// doing nothing because it returned false
		}
		// DO SOME FAST RECOVERY
	}()
	// SOME LONG RUNNING EXECUTION
}

func (*Task) Name

func (t *Task) Name() string

Name returns the name of the function

func (*Task) Stop

func (t *Task) Stop()

func (*Task) String

func (t *Task) String() string

func (*Task) Wait

func (t *Task) Wait()

type TaskFunc

type TaskFunc func(t *Task) error

TaskFunc is the executable part of the program. You can modify the timer of the task and also the functions themselves! See TaskInitFunc, NewTask and router.RegisterBackgroundTask for the creation of a Task

type TaskInitFunc

type TaskInitFunc func() (initF, execF, cleanupF TaskFunc)

TaskInitFunc is called when creating a task and is provided by the user. This function need to return 3 TaskFunc (they can be nil) and they will be set to the created task. The kind of functions are:

  • the initialization function: called only upon creation, if it fails (panics or returns an error) the task will be disabled automatically
  • the exec function: called every time, could be interrupted if the server is shutting down; in this case, you will receive a signal on Task.ListenForExit, after that you will have 10 seconds before the server will call the cleanup function and exit
  • the cleanup function: called when the server is shutting down, this must not be potentially blocking (must end in a reasonable time)

Example of usage:

 func() {
	taskInitF := func() (initF, execF, cleanupF TaskFunc) {
		var myNeededValiable package.AnyType
		initF = func(tm *server.TaskManager, t *server.Task) {
			myNeededVariable = package.InitializeNewValiable()
			// DO SOME OTHER STUFF WITH router AND t
		}
		execF = func(tm *server.TaskManager, t *server.Task) {
			myNeededVariable.UseValiable()
			// DO SOME OTHER STUFF WITH router AND t
		}
		cleaunpF = func(tm *server.TaskManager, t *server.Task) {
			// DO SOME OTHER STUFF WITH router AND t
			myNeededVariable.DestroyValiable()
		}
		return
	}
	task := tm.NewTask("myTask", taskInitF, server.TaskTimerInactive)
}

type TaskManager

type TaskManager struct {
	Router *Router
	Logger *logger.Logger
	// contains filtered or unexported fields
}

TaskManager is a component of the Router that controls the execution of external processes and tasks registered by the user

func (*TaskManager) ExecTask

func (tm *TaskManager) ExecTask(name string) error

ExecTask runs the Task immediatly

func (*TaskManager) FindProcess

func (tm *TaskManager) FindProcess(name string) (*process.Process, error)

FindProcess finds if a process with the given name is registered in the process map

func (*TaskManager) GetProcess

func (tm *TaskManager) GetProcess(name string) *process.Process

GetProcess returns the process registered with the given name, or nil if not found

func (*TaskManager) GetProcessesNames

func (tm *TaskManager) GetProcessesNames() []string

GetProcessesNames returns a slice containing all the names of the registered processes

func (*TaskManager) GetTask

func (tm *TaskManager) GetTask(name string) *Task

func (*TaskManager) GetTasksNames

func (tm *TaskManager) GetTasksNames() []string

GetTasksNames returns all the names of the registered tasks in the TaskManager

func (*TaskManager) KillProcess

func (tm *TaskManager) KillProcess(name string) error

KillProcess forcibly kills the process with the given name

func (*TaskManager) NewProcess

func (tm *TaskManager) NewProcess(name, dir string, execName string, args ...string) (*process.Process, error)

NewProcess creates a new Process with the given parameters. The process name must be a unique. It's possible to wait for its termination on multiple goroutines by calling the Wait method, and craceful shutdown is implemented in every operating system

The underlying process is described in the package github.com/nixpare/process

func (*TaskManager) NewTask

func (tm *TaskManager) NewTask(name string, f TaskInitFunc, timer TaskTimer) error

NewTask creates and registers a new Task with the given name, displayName, initialization function (f TaskInitFunc) and execution timer, the TaskManager initialize it calling the initF function provided by f (if any). If it returns an error the Task will not be registered in the TaskManager.

func (*TaskManager) ProcessIsRunning

func (tm *TaskManager) ProcessIsRunning(name string) (bool, error)

ProcessIsRunning tells if the process is running or not

func (*TaskManager) RemoveTask

func (tm *TaskManager) RemoveTask(name string) error

RemoveTask stops the task, if running, runs the cleanup function provided and removes the Task from the TaskManager

func (*TaskManager) RestartProcess

func (tm *TaskManager) RestartProcess(name string) error

RestartProcess first gracefully stops the process (not implemented, see StopProcess method) and then starts it again

func (*TaskManager) StartProcess

func (tm *TaskManager) StartProcess(name string) error

StartProcess starts an already registered process if it's not running. This method just waits for the successful start-up of the process, but It does not wait for the termination. For this, call the Wait method.

Also, this function starts the Process enabling the pipe for the standard input and the capture of the standard output, disables any real input/output and automatically logs an error if the exit status is not successfull. You can always manually call the Start method on the Process

func (*TaskManager) StopProcess

func (tm *TaskManager) StopProcess(name string) error

StopProcess tries to gracefully stop the process with the given name

func (*TaskManager) StopTask

func (tm *TaskManager) StopTask(name string) error

StopTask stops the task and wait for the exit

func (*TaskManager) WaitProcess

func (tm *TaskManager) WaitProcess(name string) (process.ExitStatus, error)

WaitProcess waits for the termination of the process and returns process information

type TaskTimer

type TaskTimer int

TaskTimer tells the TaskManager how often a Task should be executed. See the constants for the values accepted by the TaskManager

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL