diff --git a/config/config.go b/config/config.go index 7bee863..51bba3a 100644 --- a/config/config.go +++ b/config/config.go @@ -1,315 +1,316 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ // Package config holds and assists in the configuration of a writefreely instance. package config import ( "net/url" "strings" "github.com/go-ini/ini" "github.com/writeas/web-core/log" "golang.org/x/net/idna" ) const ( // FileName is the default configuration file name FileName = "config.ini" UserNormal UserType = "user" UserAdmin = "admin" ) type ( UserType string // ServerCfg holds values that affect how the HTTP server runs ServerCfg struct { HiddenHost string `ini:"hidden_host"` Port int `ini:"port"` Bind string `ini:"bind"` TLSCertPath string `ini:"tls_cert_path"` TLSKeyPath string `ini:"tls_key_path"` Autocert bool `ini:"autocert"` TemplatesParentDir string `ini:"templates_parent_dir"` StaticParentDir string `ini:"static_parent_dir"` PagesParentDir string `ini:"pages_parent_dir"` KeysParentDir string `ini:"keys_parent_dir"` HashSeed string `ini:"hash_seed"` GopherPort int `ini:"gopher_port"` Dev bool `ini:"-"` } // DatabaseCfg holds values that determine how the application connects to a datastore DatabaseCfg struct { Type string `ini:"type"` FileName string `ini:"filename"` User string `ini:"username"` Password string `ini:"password"` Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` TLS bool `ini:"tls"` } WriteAsOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` AuthLocation string `ini:"auth_location"` TokenLocation string `ini:"token_location"` InspectLocation string `ini:"inspect_location"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GitlabOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GiteaOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } SlackOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` TeamID string `ini:"team_id"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GenericOauthCfg struct { - ClientID string `ini:"client_id"` - ClientSecret string `ini:"client_secret"` - Host string `ini:"host"` - DisplayName string `ini:"display_name"` - CallbackProxy string `ini:"callback_proxy"` - CallbackProxyAPI string `ini:"callback_proxy_api"` - TokenEndpoint string `ini:"token_endpoint"` - InspectEndpoint string `ini:"inspect_endpoint"` - AuthEndpoint string `ini:"auth_endpoint"` - Scope string `ini:"scope"` - AllowDisconnect bool `ini:"allow_disconnect"` - MapUserID string `ini:"map_user_id"` - MapUsername string `ini:"map_username"` - MapDisplayName string `ini:"map_display_name"` - MapEmail string `ini:"map_email"` + ClientID string `ini:"client_id"` + ClientSecret string `ini:"client_secret"` + Host string `ini:"host"` + DisplayName string `ini:"display_name"` + CallbackProxy string `ini:"callback_proxy"` + CallbackProxyAPI string `ini:"callback_proxy_api"` + TokenEndpoint string `ini:"token_endpoint"` + InspectEndpoint string `ini:"inspect_endpoint"` + AuthEndpoint string `ini:"auth_endpoint"` + AuthUseRequestBody bool `ini:"auth_use_basic_auth"` + Scope string `ini:"scope"` + AllowDisconnect bool `ini:"allow_disconnect"` + MapUserID string `ini:"map_user_id"` + MapUsername string `ini:"map_username"` + MapDisplayName string `ini:"map_display_name"` + MapEmail string `ini:"map_email"` } // AppCfg holds values that affect how the application functions AppCfg struct { SiteName string `ini:"site_name"` SiteDesc string `ini:"site_description"` Host string `ini:"host"` // Site appearance Theme string `ini:"theme"` Editor string `ini:"editor"` JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` Landing string `ini:"landing"` SimpleNav bool `ini:"simple_nav"` WFModesty bool `ini:"wf_modesty"` // Site functionality Chorus bool `ini:"chorus"` Forest bool `ini:"forest"` // The admin cares about the forest, not the trees. Hide unnecessary technical info. DisableDrafts bool `ini:"disable_drafts"` // Users SingleUser bool `ini:"single_user"` OpenRegistration bool `ini:"open_registration"` OpenDeletion bool `ini:"open_deletion"` MinUsernameLen int `ini:"min_username_len"` MaxBlogs int `ini:"max_blogs"` // Options for public instances // Federation Federation bool `ini:"federation"` PublicStats bool `ini:"public_stats"` Monetization bool `ini:"monetization"` NotesOnly bool `ini:"notes_only"` // Access Private bool `ini:"private"` // Additional functions LocalTimeline bool `ini:"local_timeline"` UserInvites string `ini:"user_invites"` // Defaults DefaultVisibility string `ini:"default_visibility"` // Check for Updates UpdateChecks bool `ini:"update_checks"` // Disable password authentication if use only Oauth DisablePasswordAuth bool `ini:"disable_password_auth"` } EmailCfg struct { // SMTP configuration values Host string `ini:"smtp_host"` Port int `ini:"smtp_port"` Username string `ini:"smtp_username"` Password string `ini:"smtp_password"` EnableStartTLS bool `ini:"smtp_enable_start_tls"` // Mailgun configuration values Domain string `ini:"domain"` MailgunPrivate string `ini:"mailgun_private"` MailgunEurope bool `ini:"mailgun_europe"` } // Config holds the complete configuration for running a writefreely instance Config struct { Server ServerCfg `ini:"server"` Database DatabaseCfg `ini:"database"` App AppCfg `ini:"app"` Email EmailCfg `ini:"email"` SlackOauth SlackOauthCfg `ini:"oauth.slack"` WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"` GenericOauth GenericOauthCfg `ini:"oauth.generic"` } ) // New creates a new Config with sane defaults func New() *Config { c := &Config{ Server: ServerCfg{ Port: 8080, Bind: "localhost", /* IPV6 support when not using localhost? */ }, App: AppCfg{ Host: "http://localhost:8080", Theme: "write", WebFonts: true, SingleUser: true, MinUsernameLen: 3, MaxBlogs: 1, Federation: true, PublicStats: true, }, } c.UseMySQL(true) return c } // UseMySQL resets the Config's Database to use default values for a MySQL setup. func (cfg *Config) UseMySQL(fresh bool) { cfg.Database.Type = "mysql" if fresh { cfg.Database.Host = "localhost" cfg.Database.Port = 3306 } } // UseSQLite resets the Config's Database to use default values for a SQLite setup. func (cfg *Config) UseSQLite(fresh bool) { cfg.Database.Type = "sqlite3" if fresh { cfg.Database.FileName = "writefreely.db" } } // IsSecureStandalone returns whether or not the application is running as a // standalone server with TLS enabled. func (cfg *Config) IsSecureStandalone() bool { return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != "" } func (ac *AppCfg) LandingPath() string { if !strings.HasPrefix(ac.Landing, "/") { return "/" + ac.Landing } return ac.Landing } func (lc EmailCfg) Enabled() bool { return (lc.Domain != "" && lc.MailgunPrivate != "") || lc.Username != "" && lc.Password != "" && lc.Host != "" && lc.Port > 0 } func (ac AppCfg) SignupPath() string { if !ac.OpenRegistration { return "" } if ac.Chorus || ac.Private || (ac.Landing != "" && ac.Landing != "/") { return "/signup" } return "/" } // Load reads the given configuration file, then parses and returns it as a Config. func Load(fname string) (*Config, error) { if fname == "" { fname = FileName } cfg, err := ini.Load(fname) if err != nil { return nil, err } // Parse INI file uc := &Config{} err = cfg.MapTo(uc) if err != nil { return nil, err } // Do any transformations u, err := url.Parse(uc.App.Host) if err != nil { return nil, err } d, err := idna.ToASCII(u.Hostname()) if err != nil { log.Error("idna.ToASCII for %s: %s", u.Hostname(), err) return nil, err } uc.App.Host = u.Scheme + "://" + d if u.Port() != "" { uc.App.Host += ":" + u.Port() } return uc, nil } // Save writes the given Config to the given file. func Save(uc *Config, fname string) error { cfg := ini.Empty() err := ini.ReflectFrom(cfg, uc) if err != nil { return err } if fname == "" { fname = FileName } return cfg.SaveTo(fname) } diff --git a/oauth_generic.go b/oauth_generic.go index 6fa09d7..e04181e 100644 --- a/oauth_generic.go +++ b/oauth_generic.go @@ -1,145 +1,150 @@ /* * Copyright © 2020-2021 Musing Studio LLC and respective authors. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "context" "errors" "fmt" "net/http" "net/url" "strings" "github.com/writeas/web-core/log" ) type genericOauthClient struct { - ClientID string - ClientSecret string - AuthLocation string - ExchangeLocation string - InspectLocation string - CallbackLocation string - Scope string - MapUserID string - MapUsername string - MapDisplayName string - MapEmail string - HttpClient HttpClient + ClientID string + ClientSecret string + AuthUseRequestBody bool + AuthLocation string + ExchangeLocation string + InspectLocation string + CallbackLocation string + Scope string + MapUserID string + MapUsername string + MapDisplayName string + MapEmail string + HttpClient HttpClient } var _ oauthClient = genericOauthClient{} const ( genericOauthDisplayName = "OAuth" ) func (c genericOauthClient) GetProvider() string { return "generic" } func (c genericOauthClient) GetClientID() string { return c.ClientID } func (c genericOauthClient) GetCallbackLocation() string { return c.CallbackLocation } func (c genericOauthClient) buildLoginURL(state string) (string, error) { u, err := url.Parse(c.AuthLocation) if err != nil { return "", err } q := u.Query() q.Set("client_id", c.ClientID) q.Set("redirect_uri", c.CallbackLocation) q.Set("response_type", "code") q.Set("state", state) q.Set("scope", c.Scope) u.RawQuery = q.Encode() return u.String(), nil } func (c genericOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { form := url.Values{} - form.Add("client_id", c.ClientID) - form.Add("client_secret", c.ClientSecret) + if c.AuthUseRequestBody { + form.Add("client_id", c.ClientID) + form.Add("client_secret", c.ClientSecret) + } form.Add("grant_type", "authorization_code") form.Add("redirect_uri", c.CallbackLocation) form.Add("scope", c.Scope) form.Add("code", code) req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(c.ClientID, c.ClientSecret) + if !c.AuthUseRequestBody { + req.SetBasicAuth(c.ClientID, c.ClientSecret) + } resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to exchange code for access token") } var tokenResponse TokenResponse if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { return nil, err } if tokenResponse.Error != "" { return nil, errors.New(tokenResponse.Error) } return &tokenResponse, nil } func (c genericOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { req, err := http.NewRequest("GET", c.InspectLocation, nil) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to inspect access token") } // since we don't know what the JSON from the server will look like, we create a // generic interface and then map manually to values set in the config var genericInterface map[string]interface{} if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &genericInterface); err != nil { return nil, err } // map each relevant field in inspectResponse to the mapped field from the config var inspectResponse InspectResponse inspectResponse.UserID, _ = genericInterface[c.MapUserID].(string) if inspectResponse.UserID == "" { log.Error("[CONFIGURATION ERROR] Generic OAuth provider returned empty UserID value (`%s`).\n Do you need to configure a different `map_user_id` value for this provider?", c.MapUserID) return nil, fmt.Errorf("no UserID (`%s`) value returned", c.MapUserID) } inspectResponse.Username, _ = genericInterface[c.MapUsername].(string) inspectResponse.DisplayName, _ = genericInterface[c.MapDisplayName].(string) inspectResponse.Email, _ = genericInterface[c.MapEmail].(string) return &inspectResponse, nil }