diff --git a/README.md b/README.md index 4687119..d924b6c 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Home of all aah framework CLI tools. ## aah CLI Tool [![Build Status](https://travis-ci.org/go-aah/tools.svg?branch=master)](https://travis-ci.org/go-aah/tools) [![Go Report Card](https://goreportcard.com/badge/aahframework.org/tools.v0)](https://goreportcard.com/report/aahframework.org/tools.v0/aah) [![Powered by Go](https://img.shields.io/badge/powered_by-go-blue.svg)](https://golang.org) - [![Version](https://img.shields.io/badge/version-0.8-blue.svg)](https://github.com/go-aah/tools/releases/latest) + [![Version](https://img.shields.io/badge/version-0.9-blue.svg)](https://github.com/go-aah/tools/releases/latest) [![License](https://img.shields.io/github/license/go-aah/tools.svg)](LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@aahframework-55acee.svg)](https://twitter.com/aahframework) -***Release [v0.8](https://github.com/go-aah/tools/releases/latest) tagged on Sep 01, 2017*** +***Release [v0.9](https://github.com/go-aah/tools/releases/latest) tagged on Oct 04, 2017*** aah framework - A scalable, performant, rapid development Web framework for Go. diff --git a/aah/aah.go b/aah/aah.go index 7380b50..fd4a483 100644 --- a/aah/aah.go +++ b/aah/aah.go @@ -32,10 +32,10 @@ import ( ) const ( - aahImportPath = "aahframework.org/aah.v0" - aahCLIImportPath = "aahframework.org/tools.v0/aah" - permRWXRXRX = 0755 - permRWRWRW = 0666 + permRWXRXRX = 0755 + permRWRWRW = 0666 + versionSeries = "v0" + importPrefix = "aahframework.org" ) var ( @@ -43,6 +43,9 @@ var ( gocmd string gosrcDir string + libNames = []string{"aah", "ahttp", "aruntime", "config", "essentials", "forge", "i18n", + "log", "router", "security", "test", "tools", "valpar", "view"} + // abstract it, so we can do unit test fatal = log.Fatal fatalf = log.Fatalf @@ -102,6 +105,7 @@ func main() { buildCmd, listCmd, cleanCmd, + switchCmd, } sort.Sort(cli.FlagsByName(app.Flags)) @@ -113,7 +117,7 @@ func main() { //___________________________________ func printHeader(c *cli.Context) error { - hdr := fmt.Sprintf("aah framework v%s - https://aahframework.org", aah.Version) + hdr := fmt.Sprintf("aah framework v%s - https://aahframework.org", aah.Version) improveRpt := "# Report improvements/bugs at https://github.com/go-aah/aah/issues #" cnt := len(improveRpt) sp := (cnt - len(hdr)) / 2 @@ -122,13 +126,13 @@ func printHeader(c *cli.Context) error { fmt.Fprintf(c.App.Writer, "\033[1;32m") } - printChr(c.App.Writer, "–", cnt) + printChr(c.App.Writer, "‾", cnt) fmt.Fprintf(c.App.Writer, "\n") printChr(c.App.Writer, " ", sp) fmt.Fprintf(c.App.Writer, hdr) printChr(c.App.Writer, " ", sp) fmt.Fprintf(c.App.Writer, "\n") - printChr(c.App.Writer, "–", cnt) + printChr(c.App.Writer, "_", cnt) fmt.Fprintf(c.App.Writer, "\n") if !isWindowsOS() { @@ -158,18 +162,16 @@ func init() { cli.VersionPrinter = func(c *cli.Context) { _ = printHeader(c) - fmt.Fprint(c.App.Writer, "Version(s):\n") + fmt.Fprint(c.App.Writer, "Version Info:\n") fmt.Fprintf(c.App.Writer, "\t%-17s v%s\n", "aah framework", aah.Version) fmt.Fprintf(c.App.Writer, "\t%-17s v%s\n", "aah cli tool", Version) fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", "Modules: ", strings.Join( []string{ - "config v" + config.Version, "essentials v" + ess.Version, - "ahttp v" + ahttp.Version, "router v" + router.Version, - "security v" + security.Version}, ", ")) + "ahttp v" + ahttp.Version, "aruntime v" + aruntime.Version, "config v" + config.Version, + "essentials v" + ess.Version, "i18n v" + i18n.Version, "log v" + log.Version}, ", ")) fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", "", strings.Join( - []string{"i18n v" + i18n.Version, "view v" + view.Version, - "log v" + log.Version, "test v" + test.Version, - "aruntime v" + aruntime.Version, "valpar v" + valpar.Version}, ", ")) + []string{"router v" + router.Version, "security v" + security.Version, + "test v" + test.Version, "valpar v" + valpar.Version, "view v" + view.Version}, ", ")) fmt.Println() fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", fmt.Sprintf("go[%s/%s]", runtime.GOOS, runtime.GOARCH), runtime.Version()[2:]) diff --git a/aah/app-template/aah.project.atmpl b/aah/app-template/aah.project.atmpl index 59beac8..791d7bb 100644 --- a/aah/app-template/aah.project.atmpl +++ b/aah/app-template/aah.project.atmpl @@ -21,10 +21,6 @@ build { # Default value is `false`. #dep_get = true - # Log level is used for aah CLI tool logging. - # Default value is `info`. - #log_level = "info" - flags = ["-i"] ldflags = "" @@ -42,6 +38,17 @@ build { excludes = ["*.go", "*_test.go", ".*", "*.bak", "*.tmp", "vendor", "app", "build", "tests", "logs"] } +# Logger configuration for aah CLI tool. +log { + # Log level + # Default value is `info`. + #level = "info" + + # Log colored output + # Default value is `true`. + #color = false +} + # Hot-Reload is development purpose to help developer. # Read more about implementation here - https://github.com/go-aah/aah/issues/4 # diff --git a/aah/app-template/app/security/authentication_provider.go b/aah/app-template/app/security/authentication_provider.go new file mode 100644 index 0000000..7c23ad2 --- /dev/null +++ b/aah/app-template/app/security/authentication_provider.go @@ -0,0 +1,68 @@ +package security + +import ( + "aahframework.org/aah.v0" + "aahframework.org/config.v0" + "aahframework.org/security.v0/authc" +) + +var _ authc.Authenticator = (*AuthenticationProvider)(nil) + +// AuthenticationProvider struct implements `authc.Authenticator` interface. +type AuthenticationProvider struct { +} + +// Init method initializes the AuthenticationProvider, this method gets called +// during server start up. +func (a *AuthenticationProvider) Init(cfg *config.Config) error { + + // NOTE: Init is called on application startup + + return nil +} + +// GetAuthenticationInfo method is `authc.Authenticator` interface +func (a *AuthenticationProvider) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) (*authc.AuthenticationInfo, error) { + + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + // This code snippet provided as a reference + // + // Call your appropriate datasource here (such as DB, API, LDAP, etc) + // to get the subject (aka user) authentication information. + // + // Form Auth Values from authcToken + // authcToken.Identity => username + // authcToken.Credential => passowrd + //_____________________________________________________________________ + + // user := models.FindUserByEmail(authcToken.Identity) + // if user == nil { + // // No subject exists, return nil and error + // return nil, authc.ErrSubjectNotExists + // } + + // User found, now create authentication info and return to the framework + authcInfo := authc.NewAuthenticationInfo() + // authcInfo.Principals = append(authcInfo.Principals, + // &authc.Principal{ + // Value: user.Email, + // IsPrimary: true, + // Realm: "inmemory", + // }) + // authcInfo.Credential = []byte(user.Password) + // authcInfo.IsLocked = user.IsLocked + // authcInfo.IsExpired = user.IsExpried + + return authcInfo, nil +} + +func postAuthEvent(e *aah.Event) { + ctx := e.Data.(*aah.Context) + + // Do post successful authentication actions... + _ = ctx +} + +func init() { + aah.OnPostAuth(postAuthEvent) +} diff --git a/aah/app-template/app/security/authorization_provider.go b/aah/app-template/app/security/authorization_provider.go new file mode 100644 index 0000000..6f45ac6 --- /dev/null +++ b/aah/app-template/app/security/authorization_provider.go @@ -0,0 +1,43 @@ +package security + +import ( + "aahframework.org/config.v0" + "aahframework.org/security.v0/authc" + "aahframework.org/security.v0/authz" +) + +var _ authz.Authorizer = (*AuthorizationProvider)(nil) + +// AuthorizationProvider struct implements `authz.Authorizer` interface. +type AuthorizationProvider struct { +} + +// Init method initializes the AuthorizationProvider, this method gets called +// during server start up. +func (a *AuthorizationProvider) Init(cfg *config.Config) error { + + // NOTE: Init is called on application startup + + return nil +} + +// GetAuthorizationInfo method is `authz.Authorizer` interface. +// +// GetAuthorizationInfo method gets called after authentication is successful +// to get Subject's (aka User) access control information such as roles and permissions. +func (a *AuthorizationProvider) GetAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo { + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + // This code snippet provided as a reference + // + // Call your appropriate datasource here (such as DB, API, etc) + // to get the subject (aka user) authorization details (roles, permissions) + //__________________________________________________________________________ + + // authorities := models.FindUserByEmail(authcInfo.PrimaryPrincipal().Value) + + authzInfo := authz.NewAuthorizationInfo() + // authzInfo.AddRole(authorities.Roles...) + // authzInfo.AddPermissionString(authorities.Permissions...) + + return authzInfo +} diff --git a/aah/app-template/config/aah.conf.atmpl b/aah/app-template/config/aah.conf.atmpl index 478579e..d8ba5af 100644 --- a/aah/app-template/config/aah.conf.atmpl +++ b/aah/app-template/config/aah.conf.atmpl @@ -12,6 +12,14 @@ name = "{{ .AppName }}" # Friendly description of application desc = "aah framework {{ .AppType }} application" +# Application instance name is used when you're running aah application cluster. +# This value is used in the context based logging, it distinguishes your instance +# log from other instances. +# +# Typically you can to pass `instance_name` value via aah external config +# support or Environment variable. +instance_name = $AAH_INSTANCE_NAME + # Configure file path of application PID file to be written. # Ensure application has appropriate permission and directory exists. # Default value is `/.pid` @@ -26,7 +34,7 @@ server { # Default value is `empty` string. #address = "" - # For port `80` and `443`, put empty string or a value + # For standard port `80` and `443`, put empty string or a value # Default value is 8080. #port = "" @@ -79,6 +87,27 @@ server { # Default value is `false`. #disable_http2 = true + # Redirect HTTP => HTTPS functionality does protocol switch, so it works + # with domain and subdomains. + # For example: + # http://aahframework.org => https://aahframework.org + # http://www.aahframework.org => https://www.aahframework.org + # http://docs.aahframework.org => https://docs.aahframework.org + redirect_http { + # Enabling HTTP => HTTPS redirects. + # Default is value is `false`. + #enable = true + + # Port no. of HTTP requests to listen. + # For standard port `80` put empty string or a value. + # It is required value, no default. + port = "8080" + + # Redirect code + # Default value is `307`. + #code = 307 + } + lets_encrypt { # To get SSL certificate from Let's Encrypt CA, enable it. # Don't forget to enable `server.ssl.enable=true`. @@ -117,9 +146,12 @@ server { } } + # -------------------------------------------------------------------------- # To manage aah server effectively it is necessary to know details about the # request, response, processing time, client IP address, etc. aah framework # provides the flexible and configurable access log capabilities. + # Doc: https://docs.aahframework.org/server-access-log.html + # -------------------------------------------------------------------------- access_log { # Enabling server access log # Default value is `false`. @@ -140,6 +172,31 @@ server { # Default value is `true`. #static_file = false } + + # ------------------------------------------------------- + # Dump Request & Response Details + # Such as URL, Proto, Headers, Body, etc. + # Note: Dump is not applicable for Static Files delivery. + # Doc: https://docs.aahframework.org/server-dump-log.html + # ------------------------------------------------------- + dump_log { + # Default value is `false`. + #enable = true + + # Absolute path to dump log file or relative path. + # Default location is application logs directory + #file = "{{ .AppName }}-dump.log" + + # Log Request body into dump log. aah dumps body for JSON, XML, Form + # HTML and Plain Text content types. + # Default value is `false`. + #request_body = true + + # Log Request body into dump log. aah dumps body for JSON, XML, Form + # HTML, and Plain Text content types. + # Default value is `false`. + #response_body = true + } } # ------------------------------------------------------------------ @@ -148,7 +205,7 @@ server { # ------------------------------------------------------------------ request { # aah framework encourages to have unique `Request Id` for each incoming - # request, it helps in traceability. If request has already `X-Request-Id` + # request, it helps in traceability. If request has already `sX-Request-Id` # HTTP header then it does not generate one. id { # Default value is `true`. @@ -212,7 +269,6 @@ request { #tag_name = "bind" } } - {{ if eq .AppType "web" -}} # --------------------------------------------------------------- # i18n configuration @@ -309,7 +365,6 @@ render { #level = 4 } } - {{ if eq .AppType "web" -}} # ------------------------------------------------------------------ # Cache configuration @@ -369,8 +424,7 @@ view { # So option to disable the default layout for HTML. # Default value is `true`. Available since v0.6 #default_layout = false -} -{{- end }} +}{{ end }} # -------------------------------------------------------------- # Application Security diff --git a/aah/app-template/config/env/dev.conf.atmpl b/aah/app-template/config/env/dev.conf.atmpl index 522c70f..43598ab 100644 --- a/aah/app-template/config/env/dev.conf.atmpl +++ b/aah/app-template/config/env/dev.conf.atmpl @@ -6,16 +6,16 @@ dev { # -------------------------------------------------- # Log Configuration - # Doc: https://docs.aahframework.org/log-config.html + # Doc: https://docs.aahframework.org/logging.html # -------------------------------------------------- log { - # Receiver is where is log values gets logged. Currently framework - # supports `console` and `file`. Hooks for extension. + # Receiver is where is log values gets logged. aah + # supports `console` and `file` receivers. Hooks for extension. # Default value is `console`. #receiver = "file" - # Level indicates the logging levels like `ERROR`, `WARN`, `INFO`, `DEBUG` - # and `TRACE`. Config value can be in lowercase or uppercase. + # Level indicates the logging levels like `ERROR`, `WARN`, `INFO`, `DEBUG`, + # `TRACE`, FATAL and PANIC. Config value can be in lowercase or uppercase. # Default value is `debug`. level = "info" @@ -26,8 +26,12 @@ dev { # Pattern config defines the message flags and formatting while logging # into receivers. Customize it as per your need, learn more about flags # and format - https://docs.aahframework.org/log-config.html#pattern - # Default value is `%time:2006-01-02 15:04:05.000 %level:-5 %message` - pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %message" + # Default value is `%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields` + #pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields" + + # Log colored output, applicable only to `console` receiver type. + # Default value is `true`. + #color = false } # ------------------------- diff --git a/aah/app-template/config/env/prod.conf.atmpl b/aah/app-template/config/env/prod.conf.atmpl index c9dd415..bc14244 100644 --- a/aah/app-template/config/env/prod.conf.atmpl +++ b/aah/app-template/config/env/prod.conf.atmpl @@ -6,16 +6,16 @@ prod { # -------------------------------------------------- # Log Configuration - # Doc: https://docs.aahframework.org/log-config.html + # Doc: https://docs.aahframework.org/logging.html # -------------------------------------------------- log { - # Receiver is where is log values gets logged. Currently framework - # supports `console` and `file`. Hooks for extension. + # Receiver is where is log values gets logged. aah + # supports `console` and `file` receivers. Hooks for extension. # Default value is `console`. receiver = "file" - # Level indicates the logging levels like `ERROR`, `WARN`, `INFO`, `DEBUG` - # and `TRACE`. Config value can be in lowercase or uppercase. + # Level indicates the logging levels like `ERROR`, `WARN`, `INFO`, `DEBUG`, + # `TRACE`, FATAL and PANIC. Config value can be in lowercase or uppercase. # Default value is `debug`. level = "warn" @@ -26,8 +26,8 @@ prod { # Pattern config defines the message flags and formatting while logging # into receivers. Customize it as per your need, learn more about flags # and format - https://docs.aahframework.org/log-config.html#pattern - # Default value is `%time:2006-01-02 15:04:05.000 %level:-5 %message` - #pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %message" + # Default value is `%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields` + #pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields" # File config attribute is applicable only to `file` receiver type. # Default value is `aah-log-file.log`. @@ -36,7 +36,7 @@ prod { # Rotate config section is applicable only to `file` receiver type. # Default rotation is 'daily'. rotate { - # Policy is used to determine rotate policy. Currently it supports `daily`, + # Policy is used to determine rotate policy. aah supports `daily`, # `lines` and `size` policies. # Default value is `daily`. #policy = "daily" diff --git a/aah/app-template/config/routes.conf.atmpl b/aah/app-template/config/routes.conf.atmpl index 2bb0998..1cd0e39 100644 --- a/aah/app-template/config/routes.conf.atmpl +++ b/aah/app-template/config/routes.conf.atmpl @@ -43,7 +43,6 @@ domains { # `anonymous` auth scheme. # Default value is empty string. #default_auth = "" - {{ if eq .AppType "web" -}} #---------------------------------------------------------------------------- # Static Routes Configuration @@ -61,7 +60,6 @@ domains { # Doc: https://docs.aahframework.org/routes-config.html#section-static #---------------------------------------------------------------------------- static { - # Static route name, pick a unique one public_assets { # URL 'path' for serving directory @@ -83,8 +81,14 @@ domains { # or an absolute path. If it's relative path '/static/' prefixed automatically file = "img/favicon.png" } - } - {{- end }} + + # Robots Configuration file. + # Know more: https://en.wikipedia.org/wiki/Robots_exclusion_standard + robots_txt { + path = "/robots.txt" + file = "robots.txt" + } + }{{ end }} #----------------------------------------------------------------------------- # Application routes @@ -132,7 +136,7 @@ domains { # When you want to define particular route as anonymous then define # `auth` attribute as `anonymous`. # Default value is empty string. - #auth = "" + auth = "anonymous" # Max request body size for this route. If its happen to be `MultipartForm` # then this value ignored since `request.multipart_size` config from `aah.conf` @@ -142,8 +146,19 @@ domains { # from `aah.conf` is applied. So use it for specific cases. # No default value, global value is applied. #max_body_size = "5mb" + + # Optionally you can disable Anti-CSRF check for particular route. + # There are cases you might need this option. In-general don't disable the check. + # Default value is `true`. + #anti_csrf_check = false } + {{ if eq .AppAuthScheme "form" -}}login_submit { + path ="/login" + method = "POST" + controller = "VirtualFormController" + }{{ end }} + } # end - routes } # end - localhost diff --git a/aah/app-template/config/security.conf.atmpl b/aah/app-template/config/security.conf.atmpl index afeecb7..e7c5daf 100644 --- a/aah/app-template/config/security.conf.atmpl +++ b/aah/app-template/config/security.conf.atmpl @@ -24,20 +24,18 @@ security { # information. Then framework validates the credential using password # encoder. # It is required value, no default. - #authenticator = "security/Authentication" + authenticator = "security/AuthenticationProvider" # Framework calls `Authorizer` to get Subject's authorization information, # such as Roles, Permissions. Then it populates the Subject instance. # It is required value, no default. - #authorizer = "security/Authorization" + authorizer = "security/AuthorizationProvider" # Password encoder is used to encode the given credential and then compares # it with application provide credential. - # Currently supported hashing is `bcrypt`, additional hash types (upcoming). - password_encoder { - # Default value is `bcrypt`. - #type = "bcrypt" - } + # Doc: https://docs.aahframework.org/password-encoders.html + # Default value is `bcrypt`. + password_encoder = "{{ .AppPasswordEncoder }}" # Field names are used to extract `AuthenticationToken` from request. field { @@ -89,31 +87,27 @@ security { # However aah framework does its due diligence. realm_name = "Protected" + {{ if eq .AppBasicAuthMode "file-realm" -}} # Basic auth realm file path. You can use absolute path or # environment variable to provide path. - # No default value. - file_realm = "/path/to/basic-realm-file.conf" - - ## NOTE: you can use either file realm or dynamic - + # It is required value, no default. + file_realm = "{{ .AppBasicAuthFileRealmPath }}"{{ else -}} # Framework calls `Authenticator` to get the Subject's authentication # information. Then framework validates the credential using password # encoder. # It is required value when `file_realm` not configured, no default. - #authenticator = "security/Authentication" + authenticator = "security/AuthenticationProvider" # Framework calls `Authorizer` to get Subject's authorization information, # such as Roles and Permissions. Then it populates the Subject instance. # It is required value when `file_realm` not configured, no default. - #authorizer = "security/Authorization" + authorizer = "security/AuthorizationProvider"{{ end }} # Password encoder is used to encode the given credential and then compares # it with application provide credential. - # Currently supported hashing is `bcrypt`, additional hash types (upcoming). - password_encoder { - # Default value is `bcrypt`. - #type = "bcrypt" - } + # Doc: https://docs.aahframework.org/password-encoders.html + # Default value is `bcrypt`. + password_encoder = "{{ .AppPasswordEncoder }}" }{{ end -}} {{ if eq .AppAuthScheme "generic" -}} @@ -127,14 +121,14 @@ security { # Framework calls `Authenticator` to get the Subject's authentication # information. The credential validation is not done by framework, it is - # left interface implementation. + # left to interface implementation. # It is required value, no default. - #authenticator = "security/Authentication" + authenticator = "security/AuthenticationProvider" # Framework calls `Authorizer` to get Subject's authorization information, # such as Roles and Permissions. Then it populates the Subject instance. # It is required value, no default. - #authorizer = "security/Authorization" + authorizer = "security/AuthorizationProvider" # Header names are used to extract `AuthenticationToken` from request. header { @@ -149,13 +143,89 @@ security { }{{ end }} } + # ------------------------------------------------------------ + # Password Encoders Configuration + # aah supports `bcrypt`, `scrypt`, `pbkdf2` password algorithm + # Doc: https://docs.aahframework.org/password-encoders.html + # ------------------------------------------------------------ + password_encoder { {{ if eq .AppPasswordEncoder "bcrypt" }} + # bcrypt algorithm + # + # Learn more: + # https://crackstation.net/hashing-security.htm + # https://security.stackexchange.com/a/6415 + # https://en.wikipedia.org/wiki/Bcrypt + bcrypt { + # Default value is `true` + enable = true + + # https://godoc.org/golang.org/x/crypto/bcrypt#pkg-constants + # Default value is `12`. + cost = 12 + }{{ end }} + {{ if eq .AppPasswordEncoder "scrypt" }} + # scrypt algorithm + # + # Learn more: + # https://crackstation.net/hashing-security.htm + # https://pthree.org/2016/06/28/lets-talk-password-hashing/ + # https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet + # Default values are chosen carefully to provide secure password. + scrypt { + # Default value is `false` + enable = true + + # CPU/Memory Cost + # Default value is `2^15` + #cpu_memory_cost = 32768 + + # Default value is `8` + #block_size = 8 + + # Default value is `1` + #parallelization = 1 + + # Default value is `32` + #derived_key_length = 32 + + # Default value is `24` + #salt_length = 24 + }{{ end }} + {{ if eq .AppPasswordEncoder "pbkdf2" }} + # pbkdf2 algorithm + # + # Learn more: + # https://crackstation.net/hashing-security.htm + # https://cryptosense.com/parameter-choice-for-pbkdf2/ + # https://pthree.org/2016/06/28/lets-talk-password-hashing/ + # https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet + # Default values are chosen carefully to provide secure password. + pbkdf2 { + # Default value is `false` + enable = true + + # Default value is `10000` + #iteration = 10000 + + # Default value is `32` + #derived_key_length = 32 + + # Default value is `24` + #salt_length = 24 + + # Supported SHA's are `sha-1`, `sha-224`, `sha-256`, `sha-384`, `sha-512`. + # Default value is `sha-512` + #hash_algorithm = "sha-512" + }{{ end }} + } + + {{ if eq .AppType "web" }}{{ if or (eq .AppAuthScheme "form") (eq .AppAuthScheme "basic") -}} # ----------------------------------------------------------------------- # Session configuration # HTTP state management across multiple requests. # Doc: https://docs.aahframework.org/security-config.html#section-session # ----------------------------------------------------------------------- session { - {{ if eq .AppAuthScheme "form" -}} # Session mode to choose whether HTTP session should be persisted or # destroyed at the end of the request. Supported values are `stateless` # and `stateful`. @@ -169,13 +239,11 @@ security { # add custom session store. # Default value is `cookie`. type = "{{ .AppSessionStore }}" - {{ if eq .AppSessionStore "file" -}} # Filepath is used for file store to store session file in the file system. # This is only applicable for `type = "file"`, make sure application has # Read/Write access to the directory. Provide absolute path. - filepath = "{{ .AppSessionFileStorePath }}" - {{- end }} + filepath = "{{ .AppSessionFileStorePath }}"{{ end }} } # Session ID length @@ -188,8 +256,8 @@ security { #ttl = "0" # Session cookie name prefix. - # Default value is `aah` For e.g.: `aah_session` - #prefix = "aah" + # Default value is `aah_` For e.g.: `aah_myapp_session` + prefix = "aah_{{ .AppName }}" # Default value is `empty` string. #domain = "" @@ -211,15 +279,13 @@ security { # HTTP session cookie value signing using `HMAC`. For server farm this # should be same in all instance. For HMAC sign & verify it recommend to use # key size is `32` or `64` bytes. - # Default value is `64` bytes (generated when application gets created - # using `aah new` command). + # Default value is `64` bytes (`aah new` generates strong one). sign_key = "{{ .AppSessionSignKey }}" # HTTP session cookie value encryption and decryption using `AES`. For server # farm this should be same in all instance. AES algorithm is used, valid # lengths are `16`, `24`, or `32` bytes to select `AES-128`, `AES-192`, or `AES-256`. - # Default value is `32` bytes (generated when application gets created - # using `aah new` command). + # Default value is `32` bytes (`aah new` generates strong one). enc_key = "{{ .AppSessionEncKey }}" # Cleanup Interval is used to clean the expired session objects from store. @@ -228,8 +294,56 @@ security { # `m -> minutes`, `h -> hours`. # Default value is `30m`. #cleanup_interval = "30m" - {{- end }} - } + }{{ end }}{{ end -}} + + {{ if eq .AppType "web" -}}# ------------------------------------------------------------ + # Anti-CSRF Protection + # Doc: https://docs.aahframework.org/anti-csrf-protection.html + # ------------------------------------------------------------ + anti_csrf { + # Enabling Anti-CSRF Protection. + # Default value is `true`. + #enable = true + + # Anti-CSRF secret length + # Default value is `32`. + #secret_length = 32 + + # HTTP Header name for cipher token + # Default value is `X-Anti-CSRF-Token`. + #header_name = "X-Anti-CSRF-Token" + + # Form field name for cipher token + # Default value is `anti_csrf_token`. + #form_field_name = "anti_csrf_token" + + #Anti-CSRF secure cookie prefix + # Default value is `aah`. Cookie name would be `aah_anti_csrf`. + #prefix = "aah" + + # Default value is `empty` string. + #domain = "" + + # Default value is `/`. + #path = "/" + + # Time-to-live for Anti-CSRF secret. Valid time units are "m = minutes", + # "h = hours" and 0. + # Default value is `24h`. + #ttl = "24h" + + # Anti-CSRF cookie value signing using `HMAC`. For server farm this + # should be same in all instance. For HMAC sign & verify it recommend to use + # key size is `32` or `64` bytes. + # Default value is `64` bytes (`aah new` generates strong one). + sign_key = "{{ .AppAntiCSRFSignKey }}" + + # Anti-CSRF cookie value encryption and decryption using `AES`. For server + # farm this should be same in all instance. AES algorithm is used, valid + # lengths are `16`, `24`, or `32` bytes to select `AES-128`, `AES-192`, or `AES-256`. + # Default value is `32` bytes (`aah new` generates strong one). + enc_key = "{{ .AppAntiCSRFEncKey }}" + }{{ end -}} # --------------------------------------------------------------------------- # HTTP Secure Header(s) @@ -251,7 +365,7 @@ security { # Encouraged to make use of header `Content-Security-Policy` with enhanced # policy to reduce XSS risk along with header `X-XSS-Protection`. # Default values is `1; mode=block`. - #xxssp = "1; mode=block" + {{ if eq .AppType "web" -}}#xxssp = "1; mode=block"{{ else }}xxssp = ""{{ end }} # X-Content-Type-Options # Prevent Content Sniffing or MIME sniffing. @@ -269,7 +383,7 @@ security { # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xfo # https://www.keycdn.com/blog/x-frame-options/ # Default value is `SAMEORIGIN`. - #xfo = "SAMEORIGIN" + {{ if eq .AppType "web" -}}#xfo = "SAMEORIGIN"{{ else }}xfo = "DENY"{{ end }} # Referrer-Policy # This header governs which referrer information, sent in the Referer header, should @@ -281,7 +395,7 @@ security { # https://scotthelme.co.uk/a-new-security-header-referrer-policy/ # https://www.w3.org/TR/referrer-policy/ # Default value is `no-referrer-when-downgrade`. - #rp = "no-referrer-when-downgrade" + {{ if eq .AppType "web" -}}#rp = "no-referrer-when-downgrade"{{ else }}rp = ""{{ end }} # Strict-Transport-Security (STS, aka HSTS) # STS header that lets a web site tell browsers that it should only be communicated @@ -389,6 +503,6 @@ security { # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xpcdp # https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html # Default value is `master-only`. - #xpcdp = "master-only" + {{ if eq .AppType "web" -}}#xpcdp = "master-only"{{ else }}xpcdp = ""{{ end }} } } diff --git a/aah/app-template/static/robots.txt b/aah/app-template/static/robots.txt new file mode 100644 index 0000000..9d0006b --- /dev/null +++ b/aah/app-template/static/robots.txt @@ -0,0 +1,3 @@ +# Prevents all robots visiting your site. +User-agent: * +Disallow: / diff --git a/aah/build.go b/aah/build.go index 3d2149b..bdb580e 100644 --- a/aah/build.go +++ b/aah/build.go @@ -31,7 +31,7 @@ var buildCmd = cli.Command{ Examples of short and long flags: aah build aah build -e dev - aah build -i github.com/user/appname -o /Users/jeeva -e qa + aah build -i github.com/user/appname -o /Users/jeeva -e qa aah build -i github.com/user/appname -o /Users/jeeva/aahwebsite.zip aah build --importpath github.com/user/appname --output /Users/jeeva --envprofile qa`, Action: buildAction, @@ -70,8 +70,8 @@ func buildAction(c *cli.Context) error { fatalf("aah project file error: %s", err) } - _ = log.SetLevel(projectCfg.StringDefault("build.log_level", "info")) - + initLogger(projectCfg) + log.Infof("Loading aah project file: %s", filepath.Join(aah.AppBaseDir(), aahProjectIdentifier)) log.Infof("Build starts for '%s' [%s]", aah.AppName(), aah.AppImportPath()) appBinay, err := compileApp(&compileArgs{ diff --git a/aah/clean.go b/aah/clean.go index 518e565..7e8726d 100644 --- a/aah/clean.go +++ b/aah/clean.go @@ -5,13 +5,13 @@ package main import ( + "fmt" "path/filepath" "gopkg.in/urfave/cli.v1" "aahframework.org/aah.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" ) var cleanCmd = cli.Command{ @@ -36,7 +36,6 @@ var cleanCmd = cli.Command{ } func cleanAction(c *cli.Context) error { - _ = log.SetPattern("%message") importPath := firstNonEmpty(c.String("i"), c.String("importpath")) if ess.IsStrEmpty(importPath) { importPath = importPathRelwd() @@ -55,9 +54,8 @@ func cleanAction(c *cli.Context) error { filepath.Join(appBaseDir, aah.AppName()+".pid"), ) - log.Infof("Import Path: '%v' clean successful.", importPath) - log.Info() - _ = log.SetPattern(log.DefaultPattern) + fmt.Printf("Import Path: '%v' clean successful.\n", importPath) + fmt.Println() return nil } diff --git a/aah/compile.go b/aah/compile.go index ad0f86d..a6fa447 100644 --- a/aah/compile.go +++ b/aah/compile.go @@ -80,7 +80,7 @@ func compileApp(args *compileArgs) (string, error) { } // get all the types info referred aah framework context embedded - appControllers := prg.FindTypeByEmbeddedType(fmt.Sprintf("%s.Context", aahImportPath)) + appControllers := prg.FindTypeByEmbeddedType(fmt.Sprintf("%s.Context", libImportPath("aah"))) appImportPaths := prg.CreateImportPaths(appControllers) appSecurity := appSecurity(aah.AppConfig(), appImportPaths) @@ -199,11 +199,8 @@ func checkAndGetAppDeps(appImportPath string, cfg *config.Config) error { if cfg.BoolDefault("build.dep_get", false) && len(notExistsPkgs) > 0 { log.Info("Getting application dependencies ...") - for _, pkg := range notExistsPkgs { - args := []string{"get", pkg} - if _, err := execCmd(gocmd, args, false); err != nil { - return err - } + if err := goGet(notExistsPkgs...); err != nil { + return err } } else if len(notExistsPkgs) > 0 { return fmt.Errorf("Below application dependencies does not exist, "+ @@ -267,7 +264,11 @@ func appSecurity(appCfg *config.Config, appImportPaths map[string]string) map[st func prepareAuthAlias(keyAuthAlias, auth, importPathPrefix string, appImportPaths map[string]string) string { var authAlias string - importPath := path.Join(importPathPrefix, path.Dir(auth)) + importPath := path.Dir(auth) + if strings.HasPrefix(auth, "security") { + importPath = path.Join(importPathPrefix, importPath) + } + if alias, found := appImportPaths[importPath]; found { authAlias = alias } else { @@ -292,7 +293,10 @@ package main import ( "flag" "fmt" + "os" + "os/signal" "reflect" + "syscall" "aahframework.org/aah.v0" "aahframework.org/config.v0" @@ -406,6 +410,24 @@ func main() { {{- end }} {{- end }} - aah.Start() + go aah.Start() + + // Listen to OS signal's SIGINT & SIGTERM for aah server Shutdown + sc := make(chan os.Signal, 1) + signal.Notify(sc, os.Interrupt, syscall.SIGTERM) + sig := <-sc + switch sig { + case os.Interrupt: + log.Warn("Interrupt signal received") + case syscall.SIGTERM: + log.Warn("Termination signal received") + } + + // Call aah shutdown + aah.Shutdown() + log.Info("aah application shutdown successful") + + // bye bye, see you later. + os.Exit(0) } ` diff --git a/aah/list.go b/aah/list.go index 4f6d19b..8a2f02c 100644 --- a/aah/list.go +++ b/aah/list.go @@ -5,6 +5,7 @@ package main import ( + "fmt" "os" "path/filepath" "strings" @@ -12,7 +13,6 @@ import ( "gopkg.in/urfave/cli.v1" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" ) const aahProjectIdentifier = "aah.project" @@ -27,9 +27,8 @@ var listCmd = cli.Command{ } func listAction(c *cli.Context) error { - _ = log.SetPattern("%message") - log.Infof("Scanning GOPATH: %s", filepath.Join(gopath, "...")) - log.Info() + fmt.Println("Scanning GOPATH:", filepath.Join(gopath, "...")) + fmt.Println() var aahProjects []string _ = ess.Walk(gopath, func(path string, info os.FileInfo, err error) error { @@ -49,17 +48,16 @@ func listAction(c *cli.Context) error { }) if count := len(aahProjects); count > 0 { - log.Infof("%d aah projects were found, import paths are:", count) + fmt.Printf("%d aah projects were found, import paths are:\n", count) prefix := gosrcDir + string(filepath.Separator) for _, p := range aahProjects { - log.Infof(" %s", filepath.ToSlash(strings.TrimPrefix(p, prefix))) + fmt.Printf(" %s\n", filepath.ToSlash(strings.TrimPrefix(p, prefix))) } - log.Info() + fmt.Println() return nil } - log.Info(`No aah projects was found, you can create one with 'aah new'`) - log.Info() - _ = log.SetPattern(log.DefaultPattern) + fmt.Println(`No aah projects was found, you can create one with 'aah new'`) + fmt.Println() return nil } diff --git a/aah/new.go b/aah/new.go index 1186bc8..28c3ea4 100644 --- a/aah/new.go +++ b/aah/new.go @@ -11,6 +11,7 @@ import ( "io" "io/ioutil" "os" + "path" "path/filepath" "strings" @@ -21,15 +22,16 @@ import ( ) const ( - typeWeb = "web" - typeAPI = "api" - storeCookie = "cookie" - storeFile = "file" - aahTmplExt = ".atmpl" - authForm = "form" - authBasic = "basic" - authGeneric = "generic" - authNone = "none" + typeWeb = "web" + typeAPI = "api" + storeCookie = "cookie" + storeFile = "file" + aahTmplExt = ".atmpl" + authForm = "form" + authBasic = "basic" + authGeneric = "generic" + authNone = "none" + basicFileRealm = "file-realm" ) var ( @@ -51,16 +53,17 @@ var ( ) func newAction(c *cli.Context) error { - _ = log.SetPattern("%message") - log.Info("\nWelcome to interactive way to create your aah application, press ^C to exit :)") - log.Info() - log.Info("Based on your inputs, aah CLI tool generates the aah application structure for you.") + fmt.Println("\nWelcome to interactive way to create your aah application, press ^C to exit :)") + fmt.Println() + fmt.Println("Based on your inputs, aah CLI tool generates the aah application structure for you.") // Collect data importPath := getImportPath(reader) appType := getAppType(reader) authScheme := getAuthScheme(reader, appType) - sessionStore := getSessionInfo(reader, appType) + basicAuthMode := getBasicAuthMode(reader, authScheme) + passwordEncoder := getPasswordHashAlgorithm(reader, authScheme) + sessionStore := getSessionInfo(reader, appType, authScheme) // Process it appDir := filepath.Join(gosrcDir, filepath.FromSlash(importPath)) @@ -71,21 +74,37 @@ func newAction(c *cli.Context) error { "AppType": appType, "AppImportPath": importPath, "AppAuthScheme": authScheme, + "AppBasicAuthMode": basicAuthMode, + "AppPasswordEncoder": passwordEncoder, "AppSessionStore": sessionStore, "AppSessionFileStorePath": appSessionFilepath, - "AppSessionSignKey": ess.RandomString(64), - "AppSessionEncKey": ess.RandomString(32), + "AppSessionSignKey": ess.SecureRandomString(64), + "AppSessionEncKey": ess.SecureRandomString(32), + "AppAntiCSRFSignKey": ess.SecureRandomString(64), + "AppAntiCSRFEncKey": ess.SecureRandomString(32), "TmplDemils": "{{.}}", } + if basicAuthMode == basicFileRealm { + data["AppBasicAuthFileRealmPath"] = filepath.Join(appDir, "config", "basic-realm.conf") + } else { + data["AppBasicAuthFileRealmPath"] = "/path/to/basic-realm.conf" + } + if err := createAahApp(appDir, appType, data); err != nil { fatal(err) } - log.Infof("\nYour aah %s application was created successfully at '%s'", appType, appDir) - log.Infof("You shall run your application via the command: 'aah run --importpath %s'\n", importPath) - log.Info("\nGo to https://docs.aahframework.org to learn more and customize your aah application.\n") - _ = log.SetPattern(log.DefaultPattern) + fmt.Printf("\nYour aah %s application was created successfully at '%s'\n", appType, appDir) + fmt.Printf("You shall run your application via the command: 'aah run --importpath %s'\n", importPath) + fmt.Println("\nGo to https://docs.aahframework.org to learn more and customize your aah application.") + + if basicAuthMode == basicFileRealm { + fmt.Println("\nNext step:") + fmt.Println("\tCreate basic auth realm file per your application requirements.") + fmt.Println("\tRefer to 'https://docs.aahframework.org/authentication.html#basic-auth-file-realm-format' to create basic auth realm file.") + } + fmt.Println() return nil } @@ -156,7 +175,7 @@ func getAuthScheme(reader *bufio.Reader, appType string) string { authScheme = "" } } else { - log.Error("Unsupported Auth Scheme, choose either 'form', 'basic', 'generic' or 'none'") + log.Errorf("Unsupported Auth Scheme, choose either %v or 'none'", schemeNames) authScheme = "" } } @@ -164,13 +183,57 @@ func getAuthScheme(reader *bufio.Reader, appType string) string { if authScheme == authNone { authScheme = "" } + return authScheme } -func getSessionInfo(reader *bufio.Reader, appType string) string { +func getBasicAuthMode(reader *bufio.Reader, authScheme string) string { + var basicAuthMode string + if authScheme == authBasic { + for { + basicAuthMode = readInput(reader, "\nChoose your basic auth mode (file-realm, dynamic), default is 'file-realm': ") + if ess.IsStrEmpty(basicAuthMode) || basicAuthMode == "dynamic" { + break + } else { + log.Error("Unsupported Basic auth mode") + basicAuthMode = "" + } + } + + if ess.IsStrEmpty(basicAuthMode) { + basicAuthMode = basicFileRealm + } + } + + return basicAuthMode +} + +func getPasswordHashAlgorithm(reader *bufio.Reader, authScheme string) string { + var authPasswordAlgorithm string + if authScheme == authForm || authScheme == authBasic { + for { + authPasswordAlgorithm = readInput(reader, "\nChoose your password hash algorithm (bcrypt, scrypt, pbkdf2), default is 'bcrypt': ") + + if ess.IsStrEmpty(authPasswordAlgorithm) || authPasswordAlgorithm == "bcrypt" || + authPasswordAlgorithm == "scrypt" || authPasswordAlgorithm == "pbkdf2" { + break + } else { + log.Error("Unsupported Password hash algorithm") + authPasswordAlgorithm = "" + } + } + + if ess.IsStrEmpty(authPasswordAlgorithm) { + authPasswordAlgorithm = "bcrypt" + } + } + return authPasswordAlgorithm +} + +func getSessionInfo(reader *bufio.Reader, appType, authScheme string) string { sessionStore := storeCookie - if appType == typeWeb { + if appType == typeWeb && (authScheme == authForm || authScheme == authBasic) { // Session Store for { sessionStore = readInput(reader, "\nChoose your session store (cookie or file), default is 'cookie': ") @@ -181,6 +244,7 @@ func getSessionInfo(reader *bufio.Reader, appType string) string { sessionStore = "" } } + if ess.IsStrEmpty(sessionStore) { sessionStore = storeCookie } @@ -190,7 +254,7 @@ func getSessionInfo(reader *bufio.Reader, appType string) string { } func createAahApp(appDir, appType string, data map[string]interface{}) error { - aahToolsPath, err := build.Import(aahCLIImportPath, "", build.FindOnly) + aahToolsPath, err := build.Import(path.Join(libImportPath("tools"), "aah"), "", build.FindOnly) if err != nil { fatal(err) } @@ -231,7 +295,21 @@ func createAahApp(appDir, appType string, data map[string]interface{}) error { func processSection(destDir, srcDir, dir string, data map[string]interface{}) { files, _ := ess.FilesPath(filepath.Join(srcDir, dir), true) for _, v := range files { - processFile(destDir, srcDir, v, data) + if strings.Contains(v, "/app/security/") { + authScheme := data["AppAuthScheme"].(string) + if !ess.IsStrEmpty(authScheme) && authScheme != authNone { + if authScheme == authBasic { + basicAuthMode := data["AppBasicAuthMode"].(string) + if basicAuthMode == "dynamic" { + processFile(destDir, srcDir, v, data) + } + } else { + processFile(destDir, srcDir, v, data) + } + } + } else { + processFile(destDir, srcDir, v, data) + } } } diff --git a/aah/run.go b/aah/run.go index a527a6b..8056884 100644 --- a/aah/run.go +++ b/aah/run.go @@ -69,7 +69,7 @@ var runCmd = cli.Command{ } type ( - proxy struct { + hotReload struct { ProxyURL *url.URL ProxyPort string BaseDir string @@ -79,7 +79,7 @@ type ( SSLCert string SSLKey string Args []string - Server *httputil.ReverseProxy + Proxy *httputil.ReverseProxy Process *process ProjectConfig *config.Config ChangedOrError bool @@ -125,6 +125,9 @@ func runAction(c *cli.Context) error { fatalf("aah project file error: %s", err) } + initLogger(projectCfg) + log.Infof("Loading aah project file: %s", filepath.Join(aah.AppBaseDir(), aahProjectIdentifier)) + if ess.IsStrEmpty(envProfile) { envProfile = aah.AppProfile() } @@ -141,7 +144,7 @@ func runAction(c *cli.Context) error { } appURL, _ := url.Parse(fmt.Sprintf("%s://%s:%s", scheme, address, proxyPort)) - appProxy := &proxy{ + appHotReload := &hotReload{ ProxyURL: appURL, ProxyPort: proxyPort, BaseDir: aah.AppBaseDir(), @@ -151,11 +154,11 @@ func runAction(c *cli.Context) error { SSLCert: aah.AppConfig().StringDefault("server.ssl.cert", ""), SSLKey: aah.AppConfig().StringDefault("server.ssl.key", ""), Args: appStartArgs, - Server: httputil.NewSingleHostReverseProxy(appURL), + Proxy: httputil.NewSingleHostReverseProxy(appURL), ProjectConfig: projectCfg, } - appProxy.Start() + appHotReload.Start() return nil } @@ -177,22 +180,26 @@ func runAction(c *cli.Context) error { return nil } -func (p *proxy) Start() { - // starting proxy server +func (hr *hotReload) Start() { + // Starting Hot-Reload server go func() { - p.Server.ErrorLog = log.ToGoLogger() - p.Server.ErrorLog.SetOutput(ioutil.Discard) + hr.Proxy.ErrorLog = log.ToGoLogger() + hr.Proxy.ErrorLog.SetOutput(ioutil.Discard) + hr.Proxy.Transport = http.DefaultTransport var err error - address := fmt.Sprintf("%s:%s", p.Addr, p.Port) - server := &http.Server{Addr: address, Handler: p} - server.ErrorLog = p.Server.ErrorLog + address := fmt.Sprintf("%s:%s", hr.Addr, hr.Port) + server := &http.Server{ + Addr: address, + Handler: hr, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + server.ErrorLog = hr.Proxy.ErrorLog - if p.IsSSL { - p.Server.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - err = server.ListenAndServeTLS(p.SSLCert, p.SSLKey) + if hr.IsSSL { + hr.Proxy.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + err = server.ListenAndServeTLS(hr.SSLCert, hr.SSLKey) } else { err = server.ListenAndServe() } @@ -201,29 +208,29 @@ func (p *proxy) Start() { } }() - if err := p.CompileAndStart(); err != nil { + if err := hr.CompileAndStart(); err != nil { fatal(err) } sc := make(chan os.Signal, 1) signal.Notify(sc, os.Interrupt, syscall.SIGTERM) <-sc - p.Stop() + hr.Stop() } -func (p *proxy) CompileAndStart() error { +func (hr *hotReload) CompileAndStart() error { appBinary, err := compileApp(&compileArgs{ Cmd: "RunCmd", - ProxyPort: p.ProxyPort, - ProjectCfg: p.ProjectConfig, + ProxyPort: hr.ProxyPort, + ProjectCfg: hr.ProjectConfig, AppPack: false, }) if err != nil { return err } - p.Process = &process{ - cmd: exec.Command(appBinary, p.Args...), + hr.Process = &process{ + cmd: exec.Command(appBinary, hr.Args...), nw: ¬ifyWriter{ w: os.Stdout, notify: make(chan bool), @@ -231,44 +238,45 @@ func (p *proxy) CompileAndStart() error { }, } - if err = p.Process.Start(); err != nil { + if err = hr.Process.Start(); err != nil { return err } - p.RefreshWatcher() + hr.RefreshWatcher() return nil } -func (p *proxy) Stop() { - p.Process.Stop() +func (hr *hotReload) Stop() { + hr.Process.Stop() } -func (p *proxy) RefreshWatcher() { - p.Watcher = watcher.New() +func (hr *hotReload) RefreshWatcher() { + hr.Watcher = watcher.New() watch := make(chan bool) - go startWatcher(p.ProjectConfig, p.BaseDir, p.Watcher, watch) + go startWatcher(hr.ProjectConfig, hr.BaseDir, hr.Watcher, watch) go func() { for { - p.ChangedOrError = <-watch + hr.ChangedOrError = <-watch } }() } -func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if p.ChangedOrError { +func (hr *hotReload) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if hr.ChangedOrError { log.Info("Application file change(s) detected") - p.ChangedOrError = false - p.Watcher.Close() - p.Stop() - if err := p.CompileAndStart(); err != nil { + hr.ChangedOrError = false + hr.Watcher.Close() + hr.Stop() + if err := hr.CompileAndStart(); err != nil { log.Error(err) fmt.Fprintln(w, err.Error()) - p.ChangedOrError = true + hr.ChangedOrError = true return } + waitForConnReady(hr.ProxyPort) } - p.Server.ServeHTTP(w, r) + hr.Proxy.ServeHTTP(w, r) } func startWatcher(projectCfg *config.Config, baseDir string, w *watcher.Watcher, watch chan<- bool) { @@ -383,7 +391,7 @@ func (p *process) Stop() { // so we have only option is to kill. _ = p.cmd.Process.Kill() } else { - p.nw.checkBytes = []byte("application stopped") + p.nw.checkBytes = []byte("shutdown successful") p.nw.notify = make(chan bool) _ = p.cmd.Process.Signal(os.Interrupt) // wait for process to finish or return after grace time diff --git a/aah/switch.go b/aah/switch.go new file mode 100644 index 0000000..fd51d55 --- /dev/null +++ b/aah/switch.go @@ -0,0 +1,101 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// go-aah/tools/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "path" + + "gopkg.in/urfave/cli.v1" +) + +const ( + releaseBranchName = "master" + edgeBranchName = "v0-unstable" +) + +var switchCmd = cli.Command{ + Name: "switch", + Aliases: []string{"s"}, + Usage: "Switch between aah release and edge version (beta)", + Description: `Provides an ability to switch between aah release and edge version. + + Examples of short and long flags: + aah s + aah switch + + To check which version is currently active: + aah s -w + aah switch --whoami + + Note: + - Currently it works with only GOPATH. Gradually I will add vendorize support too. + - Currently it is in beta, help with your feedback for improvements. + - It always operates on latest version, specific version is not supported.`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "w, whoami", + Usage: "To know which version is currently active", + }, + }, + Action: switchAction, +} + +func switchAction(c *cli.Context) error { + branchName := gitBranchName(libDir("aah")) + if c.Bool("w") || c.Bool("whoami") { + if branchName == releaseBranchName { + fmt.Printf("You're using aah 'release' version.\n\n") + } else { // treat every branch as 'edge' version expect branch 'master'. + fmt.Printf("You're using aah 'edge' version, your feedback is appreciated.\n\n") + } + return nil + } + + var toBranch string + var friendlyName string + if branchName == releaseBranchName { + toBranch = edgeBranchName + friendlyName = "edge" + } else { + toBranch = releaseBranchName + friendlyName = "release" + } + + fmt.Printf("Switching aah version to '%s' ...\n\n", friendlyName) + + // Switch between release and edge version + for _, lib := range libNames { + dir := libDir(lib) + // Checkout the branch + if err := gitCheckout(dir, toBranch); err != nil { + fatalf("Error occurred which switching aah version: %s", err) + } + + // Refresh the branch codebase + if err := gitPull(dir); err != nil { + fatalf("Unable to refresh library: %s.%s", lib, versionSeries) + } + } + + // Refresh dependencies in grace mode + if err := goGet(path.Join(importPrefix, "aah.v0", "...")); err != nil { + fatalf("Unable to refresh dependencies: %s", err) + } + + // Install aah CLI for the switched version + args := []string{"install", path.Join(importPrefix, "tools.v0", "aah")} + if _, err := execCmd(gocmd, args, false); err != nil { + fatalf("Unable to compile CLI tool: %s", err) + } + + if toBranch == releaseBranchName { + fmt.Printf("You have successfully switched to aah 'release' version.\n\n") + } else { + fmt.Printf("You have successfully switched to aah 'edge' version, your feedback is appreciated.\n\n") + } + + return nil +} diff --git a/aah/util.go b/aah/util.go index f6e9219..cb5fe11 100644 --- a/aah/util.go +++ b/aah/util.go @@ -36,13 +36,10 @@ func importPathRelwd() string { // loadAahProjectFile method loads build config from 'aah.project' func loadAahProjectFile(baseDir string) (*config.Config, error) { - // read build config from 'aah.project' aahProjectFile := filepath.Join(baseDir, aahProjectIdentifier) if !ess.IsFileExists(aahProjectFile) { fatal("Missing 'aah.project' file, not a valid aah framework application.") } - - log.Infof("Loading aah project file: %s", aahProjectFile) return config.LoadFile(aahProjectFile) } @@ -88,13 +85,11 @@ func getAppVersion(appBaseDir string, cfg *config.Config) string { // git describe if gitcmd, err := exec.LookPath("git"); err == nil { - appGitDir := filepath.Join(appBaseDir, ".git") - if !ess.IsFileExists(appGitDir) { + if !ess.IsFileExists(filepath.Join(appBaseDir, ".git")) { return version } - _ = os.Chdir(appBaseDir) - gitArgs := []string{fmt.Sprintf("--git-dir=%s", appGitDir), "describe", "--always", "--dirty"} + gitArgs := []string{"-C", appBaseDir, "describe", "--always", "--dirty"} output, err := execCmd(gitcmd, gitArgs, false) if err != nil { return version @@ -124,7 +119,7 @@ func getBuildDate() string { func execCmd(cmdName string, args []string, stdout bool) (string, error) { cmd := exec.Command(cmdName, args...) - log.Debug("Executing ", strings.Join(cmd.Args, " ")) + log.Trace("Executing ", strings.Join(cmd.Args, " ")) if stdout { cmd.Stdout = os.Stdout @@ -215,3 +210,83 @@ func findAvailablePort() string { return strconv.Itoa(lstn.Addr().(*net.TCPAddr).Port) } + +func initLogger(cfg *config.Config) { + logLevel := cfg.StringDefault("log.level", "info") + if level, found := cfg.String("build.log_level"); found { + logLevel = level + + // DEPRECATED + log.Warnf("DEPRECATED: Config 'build.log_level' is deprecated in v0.9, use 'log.level = \"%s\"' instead. Deprecated config will not break your functionality, its good to update to latest config.", logLevel) + } + + logCfg, _ := config.ParseString("") + logCfg.SetString("log.receiver", "console") + logCfg.SetString("log.level", logLevel) + logCfg.SetBool("log.color", cfg.BoolDefault("log.color", true)) + + cliLog, _ := log.New(logCfg) + log.SetDefaultLogger(cliLog) +} + +func gitCheckout(dir, branch string) error { + if gitcmd, err := exec.LookPath("git"); err == nil { + gitArgs := []string{"-C", dir, "checkout", branch} + _, err := execCmd(gitcmd, gitArgs, false) + return err + } + return nil +} + +func libImportPath(name string) string { + return fmt.Sprintf("%s/%s.%s", importPrefix, name, versionSeries) +} + +func libDir(name string) string { + importPath := libImportPath(name) + return filepath.FromSlash(filepath.Join(gopath, "src", importPath)) +} + +func gitBranchName(dir string) string { + if !ess.IsDir(dir) { + log.Tracef("Given path '%s' is not a directory", dir) + return "" + } + + if gitcmd, err := exec.LookPath("git"); err == nil { + gitArgs := []string{"-C", dir, "rev-parse", "--abbrev-ref", "HEAD"} + output, _ := execCmd(gitcmd, gitArgs, false) + return strings.TrimSpace(output) + } + return "" +} + +func gitPull(dir string) error { + if gitcmd, err := exec.LookPath("git"); err == nil { + gitArgs := []string{"-C", dir, "pull"} + _, err := execCmd(gitcmd, gitArgs, false) + return err + } + return nil +} + +func goGet(pkgs ...string) error { + for _, pkg := range pkgs { + args := []string{"get", pkg} + if _, err := execCmd(gocmd, args, false); err != nil { + return err + } + } + return nil +} + +func waitForConnReady(port string) { + port = ":" + port + for { + if _, err := net.Dial("tcp", port); err != nil { + time.Sleep(10 * time.Millisecond) + continue + } + return + } +} diff --git a/aah/version.go b/aah/version.go index b5974ad..e2c32b7 100644 --- a/aah/version.go +++ b/aah/version.go @@ -5,4 +5,4 @@ package main // Version no. of aah framework CLI tool -const Version = "0.8" +const Version = "0.9"