package main import ( "encoding/base64" "encoding/json" "flag" "io" "io/ioutil" "net/http" "net/url" "os" "reflect" "strconv" "strings" log "github.com/sirupsen/logrus" ) var ( httpPort = flag.Int("http_port", LookupEnvOrInt("HTTP_PORT", 80), "HTTP port to serve on (defaults to 80)") logLevel = flag.String("loglevel", LookupEnvOrString("LOGLEVEL", "debug"), "log level (debug, info, warning, error) (defaults to debug)") cozyDomain = flag.String("cozy_domain", LookupEnvOrString("COZY_DOMAIN", "cozy.self-data.alpha.grandlyon.com"), "Cozy domain (defaults to cozy.self-data.alpha.grandlyon.com)") cozyRedirectURI = flag.String("cozy_redirect_uri", LookupEnvOrString("COZY_REDIRECT_URI", "/accounts/enedisgrandlyon/redirect"), "Cozy redirect URI (defaults to /accounts/enedisgrandlyon/redirect)") cozyGrdfRedirectURI = flag.String("cozy_grdf_redirect_uri", LookupEnvOrString("COZY_GRDF_REDIRECT_URI", "/accounts/grdfgrandlyon/redirect"), "Cozy redirect URI (defaults to /accounts/grdfgrandlyon/redirect)") cozyProxyURI = flag.String("cozy_proxy_uri", LookupEnvOrString("COZY_PROXY_URI", "https://oauth-proxy.self-data.alpha.grandlyon.com"), "Cozy domain (defaults to https://oauth-proxy.self-data.alpha.grandlyon.com)") ) type EnedisTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` Scope string `json:"scope"` RefreshTokenIssuedAt string `json:"refresh_token_issued_at"` IssueAt string `json:"issued_at"` UsagePointId string `json:"usage_points_id"` } type GrdfConsentement []struct { Pce string `json:"pce"` IdAccreditation string `json:"id_accreditation"` } type GrdfConsentementToken struct { AtHash string `json:"at_hash"` Sub string `json:"sub"` AuditTrackingId string `json:"auditTrackingId"` Iss string `json:"iss"` TokenName string `json:"tokenName"` Aud string `json:"aud"` CHash string `json:"c_hash"` Acr string `json:"acr"` Azp string `json:"azp"` AuthYime int `json:"auth_time"` Realm string `json:"realm"` Consentements string `json:"consentements"` Exp int `json:"exp"` TokenType string `json:"tokenType"` Iat int `json:"iat"` } type GrdfTokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` IdToken string `json:"id_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` Scope string `json:"scope"` Pce string `json:"pce"` } func LookupEnvOrString(key string, defaultVal string) string { if val, ok := os.LookupEnv(key); ok { return val } return defaultVal } func LookupEnvOrInt(key string, defaultVal int) int { if val, ok := os.LookupEnv(key); ok { v, err := strconv.Atoi(val) if err != nil { log.Fatalf("LookupEnvOrInt[%s]: %v", key, err) } return v } return defaultVal } func findItem(arrayType interface{}, item interface{}) bool { arr := reflect.ValueOf(arrayType) if arr.Kind() != reflect.Array { panic("Invalid data-type") } for i := 0; i < arr.Len(); i++ { if arr.Index(i).Interface() == item { return true } } return false } func logRoute(key string) { // format is : Subject - action - location - description switch key { case "Ping_enedis_new_auth": log.Debug("Enedis - Received - Authorize - new /auth request from Cozy") case "Ping_enedis_redirect": log.Debug("Enedis - Received - Redirect") case "Ping_enedis_new_token": log.Debug("Enedis - Received - Token - new /token request from Cozy") case "Ping_enedis_oauth_token": log.Debug("Enedis - Received - Token - an oauth token request from Cozy") case "Ping_enedis_refresh": log.Debug("Enedis - Received - Token - a refresh token request from Cozy") case "Ping_grdf_new_auth": log.Debug("Grdf - Received - Authorize - new /auth request from Cozy") case "Ping_grdf_redirect": log.Debug("Grdf - Received - Redirect") case "Ping_grdf_new_token": log.Debug("Grdf - Received - Token - new /token request from Cozy") case "Enedis_success_oauth": log.Debug("Enedis - Success - Oauth - oauth dance successfully done") case "Enedis_success_token": log.Debug("Enedis - Success - Token - Enedis gave a new token or refresh token to the cozy stack") case "Grdf_success": log.Debug("Grdf - Success - Oauth - oauth dance successfully done") case "Grdf_success_token": log.Debug("Grdf - Success - Token - Grdf gave a new token to the cozy stack") case "Enedis_error": log.Error("Enedis - Error - Oauth - something wrong happened with enedis") case "Enedis_redirect_error": log.Error("Enedis - Error - Redirect - an error occured with enedis redirection") case "Enedis_token_error": log.Error("Enedis - Error - Token - an error occured with enedis /token endpoint") case "Grdf_error": log.Error("Grdf - Error - Oauth - something wrong happened with grdf") case "Grdf_redirect_error": log.Error("Grdf - Error - Redirect - an error occured with grdf redirection") case "Grdf_token_error": log.Error("Grdf - Error - Token - an error occured with grdf /token endpoint") case "Proxy_error": log.Error("Proxy - Error - Oauth - something wrong happened in the proxy") case "Proxy_error_enedis_auth": log.Error("Proxy - Error - Enedis - Authorize") case "Proxy_error_grdf_auth": log.Error("Proxy - Error - Grdf - Authorize") case "Proxy_error_enedis_redirect": log.Error("Proxy - Error - Enedis - Redirect") case "Proxy_error_grdf_redirect": log.Error("Proxy - Error - Grdf - Redirect") case "Proxy_error_enedis_token": log.Error("Proxy - Error - Enedis - Token") case "Proxy_error_grdf_token": log.Error("Proxy - Error - Grdf - Token") case "Cozy_error_enedis_auth": log.Error("Cozy - Error - Enedis - Authorize") case "Cozy_error_grdf_auth": log.Error("Cozy - Error - Grdf - Authorize") default: log.Debug("logRoute was called with unknown arguments") } } func main() { // Parse the flags flag.Parse() // Init logging log.SetOutput(os.Stdout) log.SetFormatter(&log.TextFormatter{ PadLevelText: true, ForceQuote: true, DisableTimestamp: false, FullTimestamp: true, TimestampFormat: "2006-01-02 15:04:05", }) // Configure log level switch strings.ToLower(*logLevel) { case "error": log.SetLevel(log.ErrorLevel) case "warning": log.SetLevel(log.WarnLevel) case "info": log.SetLevel(log.InfoLevel) case "debug": log.SetLevel(log.DebugLevel) default: log.SetLevel(log.DebugLevel) log.Fatalf("Unknown logging level %s. Choose between debug, info, warning or error.", *logLevel) } mux := http.NewServeMux() log.Infof("Starting Server on port %d\n", *httpPort) mux.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "OK\n") }) // ENEDIS AUTH ENDPOINT mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { logRoute("Ping_enedis_new_auth") query := r.URL.Query() log.Debug("WL - EnedisReceived - Authorize - Query received from Cozy: ", query) clientId := query.Get("client_id") state := query.Get("state") responseType := "code" // here we use the redirect_uri param to transmit our stack url // We keep only the instance name to not reach the 100 max char of redirectUrl cozyOrigin := query.Get("redirect_uri") if len(clientId) > 0 && len(state) > 0 && len(cozyOrigin) > 0 { splitIndexStart := strings.Index(cozyOrigin, ":") splitIndexEnd := strings.Index(cozyOrigin, ".") if splitIndexStart > -1 && splitIndexEnd > -1 { instanceName := cozyOrigin[splitIndexStart+3 : splitIndexEnd] // DEV API // authURL := "https://gw.hml.api.enedis.fr/dataconnect/v1/oauth2/authorize" // PROD API authURL := "https://mon-compte-particulier.enedis.fr/dataconnect/v1/oauth2/authorize" redirectUrl := authURL + "?client_id=" + clientId + "&duration=P6M&response_type=" + responseType + "&state=" + state + "-" + instanceName log.Info("WL - EnedisSuccess - Authorize - Redirecting user to Enedis: ", redirectUrl) http.Redirect(w, r, redirectUrl, 302) } else { logRoute("Proxy_error_enedis_auth") log.Error("WL - ProxyError - Enedis - Authorize - redirect_uri bad format from Cozy " + cozyOrigin) http.Error(w, http.StatusText(500), 500) } } else { logRoute("Cozy_error_enedis_auth") log.Debug("WL - CozyError - Enedis - Authorize - Missing parameters in request") http.Error(w, http.StatusText(500), 500) } }) // GRDF ADICT AUTHORIZE ENDPOINT mux.HandleFunc("/grdf_authorize", func(w http.ResponseWriter, r *http.Request) { logRoute("Ping_grdf_new_auth") query := r.URL.Query() log.Debug("WL - GrdfReceived - Authorize - Query received: ", query) clientId := query.Get("client_id") state := query.Get("state") cozyOrigin := query.Get("redirect_uri") if len(clientId) > 0 && len(state) > 0 && len(cozyOrigin) > 0 { splitIndexStart := strings.Index(cozyOrigin, ":") splitIndexEnd := strings.Index(cozyOrigin, ".") if splitIndexStart > -1 && splitIndexEnd > -1 { instanceName := cozyOrigin[splitIndexStart+3 : splitIndexEnd] redirectProxy := *cozyProxyURI + "/redirect-grdf" authURL := "https://sofit-sso-oidc.grdf.fr/openam/oauth2/realms/externeGrdf/authorize" redirectUrl := authURL + "?client_id=" + clientId + "&scope=openid&response_type=code&redirect_uri=" + redirectProxy + "&login_hint=Prénom|Nom||Ecolyo&state=" + state + "-" + instanceName log.Info("WL - GrdfSuccess - Authorize - Redirect user to: ", redirectUrl) http.Redirect(w, r, redirectUrl, 302) } else { logRoute("Proxy_error_grdf_auth") log.Error("WL - ProxyError - Grdf - Authorize - redirect_uri bad format from Cozy " + cozyOrigin) http.Error(w, http.StatusText(500), 500) } } else { logRoute("Cozy_error_grdf_auth") log.Debug("WL - CozyError - Grdf - Authorize - Missing parameters in request") http.Error(w, http.StatusText(500), 500) } }) //ENEDIS REDIRECT ENDPOINT mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) { logRoute("Ping_enedis_redirect") query := r.URL.Query() log.Debug("WL - EnedisReceived - Redirect - Query received: ", query) code := query.Get("code") req_state := query.Get("state") statusCodes := [4]string{"400", "403", "500", "503"} if len(code) > 0 && len(req_state) > 0 { if findItem(statusCodes, code) { intCode, err := strconv.Atoi(code) if err != nil { logRoute("Proxy_error_enedis_redirect") log.Error("WL - ProxyError - Enedis - Redirect - String to int convert error for status code: ", err) http.Error(w, http.StatusText(500), 500) return } logRoute("Enedis_redirect_error") log.Error("WL - EnedisError - Redirect - status code error: ", code) http.Error(w, http.StatusText(intCode), intCode) return } splitIndex := strings.Index(req_state, "-") if splitIndex == -1 { logRoute("Proxy_error_enedis_redirect") log.Error("WL - ProxyError - Enedis - Redirect - No host found in query") http.Error(w, http.StatusText(500), 500) return } state := req_state[0:splitIndex] host := req_state[splitIndex+1:] usagePointId := query.Get("usage_point_id") cozyURL := "https://" + host + "." + *cozyDomain + *cozyRedirectURI redir := cozyURL + "?code=" + code + "&state=" + state + "&usage_point_id=" + usagePointId log.Info("WL - EnedisSuccess - Redirect - Redirecting user to Cozy stack: ", redir) http.Redirect(w, r, redir, 302) } else { logRoute("Proxy_error_enedis_redirect") log.Error("WL - ProxyError - Enedis - Redirect - Missing parameters in request") http.Error(w, http.StatusText(500), 500) } }) //ENEDIS WRONG REDIRECT ENDPOINT mux.HandleFunc("/redirect/", func(w http.ResponseWriter, r *http.Request) { logRoute("Enedis_redirect_error") log.Error("WL - EnedisError - Redirect - Wrong route received from Enedis ") http.Error(w, http.StatusText(500), 500) }) //GRDF REDIRECT ENDPOINT mux.HandleFunc("/redirect-grdf", func(w http.ResponseWriter, r *http.Request) { logRoute("Ping_grdf_redirect") query := r.URL.Query() log.Debug("WL - GrdfReceived - Redirect - Received redirect answer from GRDF: ", query) code := query.Get("code") req_state := query.Get("state") if len(code) > 0 && len(req_state) > 0 { splitIndex := strings.Index(req_state, "-") if splitIndex == -1 { logRoute("Proxy_error_grdf_redirect") log.Error("WL - ProxyError - Grdf - Redirect - No host found") http.Error(w, http.StatusText(500), 500) return } state := req_state[0:splitIndex] host := req_state[splitIndex+1:] cozyURL := "https://" + host + "." + *cozyDomain + *cozyGrdfRedirectURI redir := cozyURL + "?code=" + code + "&state=" + state log.Info("WL - GrdfSuccess - Redirect - Redirect to Cozy stack: ", redir) http.Redirect(w, r, redir, 302) } else { logRoute("Proxy_error_grdf_redirect") log.Error("WL - ProxyError - Grdf - Redirect - Missing parameters in request") http.Error(w, http.StatusText(500), 500) } }) //ENEDIS TOKEN ENDPOINT mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { logRoute("Ping_enedis_new_token") query := r.URL.Query() log.Debug("WL - EnedisReceived - Token - Received new token request from Cozy: ", query) clientId := "" clientSecret := "" code := "" grantType := "" refreshToken := "" // For request token params are into query parameters if len(query) == 0 { log.Debug("WL - EnedisInfo - Token - No params found in url query, trying to catch them from body") contents, err := ioutil.ReadAll(r.Body) if err != nil { logRoute("Proxy_error_enedis_token") log.Error("WL - ProxyError - Enedis - Token - Unable to read the body: ", err) http.Error(w, err.Error(), 500) return } params, err := url.ParseQuery(string(contents)) if err != nil { logRoute("Proxy_error_enedis_token") log.Error("WL - ProxyError - Enedis - Token - Unable to parse query", err) http.Error(w, err.Error(), 500) return } if val, ok := params["client_id"]; ok { clientId = val[0] } if val, ok := params["client_secret"]; ok { clientSecret = val[0] } if val, ok := params["code"]; ok { code = val[0] } if val, ok := params["grant_type"]; ok { grantType = val[0] } if val, ok := params["refresh_token"]; ok { refreshToken = val[0] } } else { // Retrieve params from query clientId = query.Get("client_id") clientSecret = query.Get("client_secret") code = query.Get("code") grantType = query.Get("grant_type") refreshToken = query.Get("refresh_token") } // Print out the result log.WithFields(log.Fields{ "client_id": clientId, "client_secret": clientSecret, "code": code, "grant_type": grantType, "refresh_token": refreshToken, }).Debug("WL - EnedisInfo - Token - Result") // DEV API // tokenUrl := "https://gw.hml.api.enedis.fr/v1/oauth2/token" // PROD API tokenUrl := "https://gw.prd.api.enedis.fr/v1/oauth2/token" data := url.Values{} data.Set("client_id", clientId) data.Set("client_secret", clientSecret) data.Set("code", code) data.Set("grant_type", grantType) if refreshToken != "" { logRoute("Ping_enedis_refresh") log.Debug("WL - EnedisReceived - Token - Cozystack asks for a new refresh token") data.Set("refresh_token", refreshToken) data.Set("grant_type", "refresh_token") } else { logRoute("Ping_enedis_oauth_token") log.Debug("WL - EnedisReceived - Token - Cozystack asks for a regular token to end the oauth dance") } log.Debug("WL - EnedisInfo - Token - Send request to Enedis token endpoint: ", tokenUrl) response, err := http.PostForm(tokenUrl, data) if err != nil { logRoute("Proxy_error_enedis_token") log.Error("WL - ProxyError - Enedis - Token - Unable to post the request: ", err) http.Error(w, http.StatusText(500), 500) return } log.Debug("WL - EnedisInfo - Token - Enedis answered back with status ", response.Status) defer response.Body.Close() if response.StatusCode >= 200 && response.StatusCode <= 299 { // Set Content-Type in response header w.Header().Add("Content-Type", "application/json") // Decode response Body using the defined type "TokenResponse" data := EnedisTokenResponse{} decodeError := json.NewDecoder(response.Body).Decode(&data) if decodeError != nil { logRoute("Proxy_error_enedis_token") log.Error("WL - ProxyError - Enedis - Token - Unable to decode data: ", decodeError) http.Error(w, decodeError.Error(), 500) return } // Response with json data jsonError := json.NewEncoder(w).Encode(data) if jsonError != nil { logRoute("Proxy_error_enedis_token") log.Error("WL - ProxyError - Enedis - Token - Unable to encode data: ", jsonError) http.Error(w, jsonError.Error(), 500) return } if refreshToken != "" { logRoute("Enedis_success_token") } else { logRoute("Enedis_success_oauth") } log.Info("WL - EnedisSuccess - Token - Respond correctly to Cozy stack") } else { logRoute("Enedis_token_error") log.Error("WL - EnedisError - Token - Enedis answer back with status code: ", response.StatusCode) http.Error(w, http.StatusText(response.StatusCode), response.StatusCode) } }) //GRDF TOKEN ENDPOINT mux.HandleFunc("/grdf_token", func(w http.ResponseWriter, r *http.Request) { logRoute("Ping_grdf_new_token") query := r.URL.Query() log.Debug("WL - GrdfReceived - Token - Received new token request from Cozy", query) clientId := "" clientSecret := "" code := "" grantType := "" scope := "" redirectUri := *cozyProxyURI + "/redirect-grdf" pce := "" IdToken := "" // For request token params are into query parameters if len(query) == 0 { log.Debug("WL - GrdfInfo - Token - No params found in url query \nStack probably asks for a refresh token \nTrying to catch them from body") contents, err := ioutil.ReadAll(r.Body) if err != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to read the body: ", err) http.Error(w, err.Error(), 500) return } params, err := url.ParseQuery(string(contents)) if err != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to parse the query: ", err) http.Error(w, err.Error(), 500) return } if val, ok := params["client_id"]; ok { clientId = val[0] } if val, ok := params["client_secret"]; ok { clientSecret = val[0] } if val, ok := params["grant_type"]; ok { grantType = val[0] } } else { // Retrieve params from query clientId = query.Get("client_id") clientSecret = query.Get("client_secret") code = query.Get("code") grantType = query.Get("grant_type") } // Print out the result log.WithFields(log.Fields{ "client_id": clientId, "client_secret": clientSecret, "code": code, "grant_type": grantType, "redirect_uri": redirectUri, "scope": scope, }).Debug("WL - GrdfInfo - Token - Result") tokenUrl := "https://sofit-sso-oidc.grdf.fr/openam/oauth2/realms/externeGrdf/access_token" if grantType != "refresh_token" { // Call GRDF access_token endpoint with code & grant_type = "authorization_code" data := url.Values{} data.Set("client_id", clientId) data.Set("client_secret", clientSecret) data.Set("grant_type", "authorization_code") data.Set("redirect_uri", redirectUri) data.Set("code", code) log.Debug("WL - GrdfInfo - Token - data sent is: ", data) log.Debug("WL - GrdfInfo - Token - Send request to access_token endpoint with authorization_code: ", tokenUrl) response, err := http.PostForm(tokenUrl, data) if err != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to post the request: ", err) http.Error(w, http.StatusText(500), 500) return } log.Debug("WL - GrdfInfo - Token - GRDF Endpoint response with status ", response.Status) defer response.Body.Close() if response.StatusCode >= 200 && response.StatusCode <= 299 { // Decode response Body using the defined type "GrdfTokenResponse" data := GrdfTokenResponse{} decodeError := json.NewDecoder(response.Body).Decode(&data) if decodeError != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to decode data: ", err) http.Error(w, decodeError.Error(), 500) return } // Check if IdToken exist // Decode the token and retrieve the pce from it if len(data.IdToken) > 0 { IdToken = data.IdToken s := strings.Split(IdToken, ".") if len(s[1]) > 0 { payload, _ := base64.StdEncoding.DecodeString(s[1]) // Check if the payload is well ended if payload[len(payload)-1] != 125 { payload = append(payload, []byte{125}...) } log.Debug("WL - GrdfInfo - Token - token decoded payload: ", string(payload)) // Decode the payload from the token var token GrdfConsentementToken err := json.Unmarshal(payload, &token) if err != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to unmarshal payload from token: ", err) http.Error(w, err.Error(), 500) return } log.Debug("WL - GrdfInfo - Token - Consentements found: ", token.Consentements) // Decode the consentement information if len(token.Consentements) > 0 { var consentements GrdfConsentement err2 := json.Unmarshal([]byte(token.Consentements), &consentements) if err2 != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to unmarshal consentement information: ", err2) http.Error(w, err2.Error(), 500) return } if len(consentements[0].Pce) > 0 { pce = consentements[0].Pce } } } } if len(pce) <= 0 { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - No PCE found") http.Error(w, http.StatusText(500), 500) return } } else { logRoute("Grdf_token_error") log.Error("WL - GrdfError - Token - GRDF response with status code: ", response.StatusCode) http.Error(w, http.StatusText(response.StatusCode), response.StatusCode) return } } // Call GRDF access_token endpoint with scope & grant_type = "client_credentials" data2 := url.Values{} data2.Set("client_id", clientId) data2.Set("client_secret", clientSecret) data2.Set("grant_type", "client_credentials") data2.Set("redirect_uri", redirectUri) data2.Set("scope", "/adict/v1") log.Debug("WL - GrdfInfo - Token - data sent is: ", data2) log.Debug("WL - GrdfInfo - Token - send request to access_token endpoint with client_credentials: ", tokenUrl) response2, err2 := http.PostForm(tokenUrl, data2) if err2 != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to post the request: ", err2) http.Error(w, http.StatusText(500), 500) return } log.Debug("WL - GrdfInfo - Token - Endpoint response with status ", response2.Status) defer response2.Body.Close() if response2.StatusCode >= 200 && response2.StatusCode <= 299 { // Set Content-Type in response header w.Header().Add("Content-Type", "application/json") // Decode response Body using the defined type "GrdfTokenResponse" data := GrdfTokenResponse{} decodeError := json.NewDecoder(response2.Body).Decode(&data) if decodeError != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to decode data: ", decodeError) http.Error(w, decodeError.Error(), 500) return } if grantType != "refresh_token" { data.RefreshToken = "-" data.Pce = pce data.IdToken = IdToken } jsonError := json.NewEncoder(w).Encode(data) if jsonError != nil { logRoute("Proxy_error_grdf_token") log.Error("WL - ProxyError - Grdf - Token - Unable to encode data: ", jsonError) http.Error(w, jsonError.Error(), 500) return } logRoute("Grdf_success_token") log.Info("WL - GrdfSuccess - Token - Response correctly to Cozy stack") } else { logRoute("Grdf_token_error") log.Error("WL - GrdfError - Token - GRDF response with status code: ", response2.StatusCode) http.Error(w, http.StatusText(response2.StatusCode), response2.StatusCode) } }) log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*httpPort), mux)) }