2014-04-10 22:20:58 +04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2014-05-26 04:11:25 +04:00
package setting
2014-04-10 22:20:58 +04:00
import (
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
2014-07-25 00:31:59 +04:00
"time"
2014-04-10 22:20:58 +04:00
"github.com/Unknwon/com"
"github.com/Unknwon/goconfig"
2014-07-26 08:24:27 +04:00
"github.com/macaron-contrib/session"
2014-04-10 22:20:58 +04:00
"github.com/gogits/gogs/modules/log"
2014-07-26 08:24:27 +04:00
// "github.com/gogits/gogs-ng/modules/ssh"
2014-04-10 22:20:58 +04:00
)
2014-05-26 04:11:25 +04:00
type Scheme string
2014-04-14 02:12:07 +04:00
2014-05-26 04:11:25 +04:00
const (
HTTP Scheme = "http"
HTTPS Scheme = "https"
)
2014-04-10 22:20:58 +04:00
var (
2014-05-26 04:11:25 +04:00
// App settings.
AppVer string
AppName string
AppUrl string
// Server settings.
Protocol Scheme
Domain string
HttpAddr , HttpPort string
SshPort int
OfflineMode bool
DisableRouterLog bool
CertFile , KeyFile string
StaticRootPath string
2014-07-26 08:24:27 +04:00
EnableGzip bool
2014-05-26 04:11:25 +04:00
// Security settings.
2014-06-24 21:55:47 +04:00
InstallLock bool
SecretKey string
LogInRememberDays int
CookieUserName string
CookieRememberName string
ReverseProxyAuthUser string
2014-04-10 22:20:58 +04:00
2014-06-08 12:45:34 +04:00
// Webhook settings.
WebhookTaskInterval int
WebhookDeliverTimeout int
2014-05-26 04:11:25 +04:00
// Repository settings.
RepoRootPath string
ScriptType string
2014-04-10 22:20:58 +04:00
2014-05-26 04:11:25 +04:00
// Picture settings.
PictureService string
DisableGravatar bool
2014-09-17 08:03:03 +04:00
MaxGitDiffLines int
2014-05-26 04:11:25 +04:00
// Log settings.
2014-05-28 09:53:06 +04:00
LogRootPath string
LogModes [ ] string
LogConfigs [ ] string
2014-04-10 22:20:58 +04:00
2014-07-23 23:15:47 +04:00
// Attachment settings.
AttachmentPath string
AttachmentAllowedTypes string
2014-07-24 17:19:59 +04:00
AttachmentMaxSize int64
AttachmentMaxFiles int
2014-07-24 17:51:40 +04:00
AttachmentEnabled bool
2014-07-23 23:15:47 +04:00
2014-07-25 00:31:59 +04:00
// Time settings.
TimeFormat string
2014-05-26 04:11:25 +04:00
// Cache settings.
2014-08-01 01:25:34 +04:00
CacheAdapter string
CacheInternal int
CacheConn string
2014-04-10 22:20:58 +04:00
2014-05-26 04:11:25 +04:00
EnableRedis bool
EnableMemcache bool
// Session settings.
2014-04-10 22:20:58 +04:00
SessionProvider string
SessionConfig * session . Config
2014-05-26 04:11:25 +04:00
// Global setting objects.
2014-07-26 08:24:27 +04:00
Cfg * goconfig . ConfigFile
ConfRootPath string
CustomPath string // Custom directory path.
ProdMode bool
RunUser string
// I18n settings.
Langs , Names [ ] string
2014-04-10 22:20:58 +04:00
)
2014-07-26 08:24:27 +04:00
func init ( ) {
log . NewLogger ( 0 , "console" , ` { "level": 0} ` )
}
2014-06-11 03:11:53 +04:00
func ExecPath ( ) ( string , error ) {
2014-05-26 04:11:25 +04:00
file , err := exec . LookPath ( os . Args [ 0 ] )
if err != nil {
return "" , err
}
p , err := filepath . Abs ( file )
if err != nil {
return "" , err
}
2014-06-11 03:11:53 +04:00
return p , nil
}
// WorkDir returns absolute path of work directory.
func WorkDir ( ) ( string , error ) {
execPath , err := ExecPath ( )
return path . Dir ( strings . Replace ( execPath , "\\" , "/" , - 1 ) ) , err
2014-05-26 04:11:25 +04:00
}
// NewConfigContext initializes configuration context.
2014-05-26 04:57:01 +04:00
// NOTE: do not print any log except error.
2014-05-26 04:11:25 +04:00
func NewConfigContext ( ) {
workDir , err := WorkDir ( )
if err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Fail to get work directory: %v" , err )
2014-05-26 04:11:25 +04:00
}
2014-07-26 08:24:27 +04:00
ConfRootPath = path . Join ( workDir , "conf" )
2014-05-26 04:11:25 +04:00
2014-07-26 08:24:27 +04:00
Cfg , err = goconfig . LoadConfigFile ( path . Join ( workDir , "conf/app.ini" ) )
2014-05-26 04:11:25 +04:00
if err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Fail to parse 'conf/app.ini': %v" , err )
2014-05-26 04:11:25 +04:00
}
CustomPath = os . Getenv ( "GOGS_CUSTOM" )
if len ( CustomPath ) == 0 {
CustomPath = path . Join ( workDir , "custom" )
}
cfgPath := path . Join ( CustomPath , "conf/app.ini" )
if com . IsFile ( cfgPath ) {
if err = Cfg . AppendFiles ( cfgPath ) ; err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Fail to load custom 'conf/app.ini': %v" , err )
2014-05-26 04:11:25 +04:00
}
} else {
2014-07-26 08:24:27 +04:00
log . Warn ( "No custom 'conf/app.ini' found, please go to '/install'" )
2014-05-26 04:11:25 +04:00
}
AppName = Cfg . MustValue ( "" , "APP_NAME" , "Gogs: Go Git Service" )
2014-08-25 05:45:39 +04:00
AppUrl = Cfg . MustValue ( "server" , "ROOT_URL" , "http://localhost:3000/" )
if AppUrl [ len ( AppUrl ) - 1 ] != '/' {
AppUrl += "/"
}
2014-05-26 04:11:25 +04:00
Protocol = HTTP
if Cfg . MustValue ( "server" , "PROTOCOL" ) == "https" {
Protocol = HTTPS
CertFile = Cfg . MustValue ( "server" , "CERT_FILE" )
KeyFile = Cfg . MustValue ( "server" , "KEY_FILE" )
}
Domain = Cfg . MustValue ( "server" , "DOMAIN" , "localhost" )
HttpAddr = Cfg . MustValue ( "server" , "HTTP_ADDR" , "0.0.0.0" )
HttpPort = Cfg . MustValue ( "server" , "HTTP_PORT" , "3000" )
SshPort = Cfg . MustInt ( "server" , "SSH_PORT" , 22 )
OfflineMode = Cfg . MustBool ( "server" , "OFFLINE_MODE" )
DisableRouterLog = Cfg . MustBool ( "server" , "DISABLE_ROUTER_LOG" )
2014-05-28 09:53:06 +04:00
StaticRootPath = Cfg . MustValue ( "server" , "STATIC_ROOT_PATH" , workDir )
LogRootPath = Cfg . MustValue ( "log" , "ROOT_PATH" , path . Join ( workDir , "log" ) )
2014-07-26 08:24:27 +04:00
EnableGzip = Cfg . MustBool ( "server" , "ENABLE_GZIP" )
2014-05-26 04:11:25 +04:00
InstallLock = Cfg . MustBool ( "security" , "INSTALL_LOCK" )
SecretKey = Cfg . MustValue ( "security" , "SECRET_KEY" )
LogInRememberDays = Cfg . MustInt ( "security" , "LOGIN_REMEMBER_DAYS" )
CookieUserName = Cfg . MustValue ( "security" , "COOKIE_USERNAME" )
CookieRememberName = Cfg . MustValue ( "security" , "COOKIE_REMEMBER_NAME" )
2014-06-24 21:55:47 +04:00
ReverseProxyAuthUser = Cfg . MustValue ( "security" , "REVERSE_PROXY_AUTHENTICATION_USER" , "X-WEBAUTH-USER" )
2014-05-26 04:11:25 +04:00
2014-07-26 08:24:27 +04:00
AttachmentPath = Cfg . MustValue ( "attachment" , "PATH" , "data/attachments" )
AttachmentAllowedTypes = Cfg . MustValue ( "attachment" , "ALLOWED_TYPES" , "image/jpeg|image/png" )
2014-07-24 17:19:59 +04:00
AttachmentMaxSize = Cfg . MustInt64 ( "attachment" , "MAX_SIZE" , 32 )
AttachmentMaxFiles = Cfg . MustInt ( "attachment" , "MAX_FILES" , 10 )
2014-07-24 17:51:40 +04:00
AttachmentEnabled = Cfg . MustBool ( "attachment" , "ENABLE" , true )
2014-07-23 23:15:47 +04:00
2014-08-01 08:24:29 +04:00
TimeFormat = map [ string ] string {
"ANSIC" : time . ANSIC ,
"UnixDate" : time . UnixDate ,
"RubyDate" : time . RubyDate ,
"RFC822" : time . RFC822 ,
"RFC822Z" : time . RFC822Z ,
"RFC850" : time . RFC850 ,
"RFC1123" : time . RFC1123 ,
"RFC1123Z" : time . RFC1123Z ,
"RFC3339" : time . RFC3339 ,
"RFC3339Nano" : time . RFC3339Nano ,
"Kitchen" : time . Kitchen ,
"Stamp" : time . Stamp ,
"StampMilli" : time . StampMilli ,
"StampMicro" : time . StampMicro ,
"StampNano" : time . StampNano ,
} [ Cfg . MustValue ( "time" , "FORMAT" , "RFC1123" ) ]
2014-07-25 00:31:59 +04:00
2014-07-23 23:15:47 +04:00
if err = os . MkdirAll ( AttachmentPath , os . ModePerm ) ; err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Could not create directory %s: %s" , AttachmentPath , err )
2014-07-23 23:15:47 +04:00
}
2014-05-26 04:11:25 +04:00
RunUser = Cfg . MustValue ( "" , "RUN_USER" )
curUser := os . Getenv ( "USER" )
if len ( curUser ) == 0 {
curUser = os . Getenv ( "USERNAME" )
}
// Does not check run user when the install lock is off.
if InstallLock && RunUser != curUser {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Expect user(%s) but current user is: %s" , RunUser , curUser )
2014-05-26 04:11:25 +04:00
}
// Determine and create root git reposiroty path.
homeDir , err := com . HomeDir ( )
if err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Fail to get home directory: %v" , err )
2014-05-26 04:11:25 +04:00
}
RepoRootPath = Cfg . MustValue ( "repository" , "ROOT" , filepath . Join ( homeDir , "gogs-repositories" ) )
2014-06-24 10:28:47 +04:00
if ! filepath . IsAbs ( RepoRootPath ) {
RepoRootPath = filepath . Join ( workDir , RepoRootPath )
} else {
RepoRootPath = filepath . Clean ( RepoRootPath )
}
2014-05-26 04:11:25 +04:00
if err = os . MkdirAll ( RepoRootPath , os . ModePerm ) ; err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Fail to create repository root path(%s): %v" , RepoRootPath , err )
2014-05-26 04:11:25 +04:00
}
ScriptType = Cfg . MustValue ( "repository" , "SCRIPT_TYPE" , "bash" )
PictureService = Cfg . MustValueRange ( "picture" , "SERVICE" , "server" ,
[ ] string { "server" } )
DisableGravatar = Cfg . MustBool ( "picture" , "DISABLE_GRAVATAR" )
2014-07-26 08:24:27 +04:00
2014-09-17 08:03:03 +04:00
MaxGitDiffLines = Cfg . MustInt ( "git" , "MAX_GITDIFF_LINES" , 5000 )
2014-07-26 08:24:27 +04:00
Langs = Cfg . MustValueArray ( "i18n" , "LANGS" , "," )
Names = Cfg . MustValueArray ( "i18n" , "NAMES" , "," )
2014-05-26 04:11:25 +04:00
}
2014-04-10 22:20:58 +04:00
var Service struct {
2014-06-21 08:51:41 +04:00
RegisterEmailConfirm bool
DisableRegistration bool
RequireSignInView bool
EnableCacheAvatar bool
EnableNotifyMail bool
EnableReverseProxyAuth bool
LdapAuth bool
ActiveCodeLives int
ResetPwdCodeLives int
2014-04-10 22:20:58 +04:00
}
2014-05-26 04:11:25 +04:00
func newService ( ) {
Service . ActiveCodeLives = Cfg . MustInt ( "service" , "ACTIVE_CODE_LIVE_MINUTES" , 180 )
Service . ResetPwdCodeLives = Cfg . MustInt ( "service" , "RESET_PASSWD_CODE_LIVE_MINUTES" , 180 )
Service . DisableRegistration = Cfg . MustBool ( "service" , "DISABLE_REGISTRATION" )
Service . RequireSignInView = Cfg . MustBool ( "service" , "REQUIRE_SIGNIN_VIEW" )
Service . EnableCacheAvatar = Cfg . MustBool ( "service" , "ENABLE_CACHE_AVATAR" )
2014-06-21 08:51:41 +04:00
Service . EnableReverseProxyAuth = Cfg . MustBool ( "service" , "ENABLE_REVERSE_PROXY_AUTHENTICATION" )
2014-04-10 22:20:58 +04:00
}
var logLevels = map [ string ] string {
"Trace" : "0" ,
"Debug" : "1" ,
"Info" : "2" ,
"Warn" : "3" ,
"Error" : "4" ,
"Critical" : "5" ,
}
func newLogService ( ) {
2014-05-11 22:37:12 +04:00
log . Info ( "%s %s" , AppName , AppVer )
2014-04-10 22:20:58 +04:00
2014-05-11 22:37:12 +04:00
// Get and check log mode.
LogModes = strings . Split ( Cfg . MustValue ( "log" , "MODE" , "console" ) , "," )
LogConfigs = make ( [ ] string , len ( LogModes ) )
for i , mode := range LogModes {
mode = strings . TrimSpace ( mode )
modeSec := "log." + mode
if _ , err := Cfg . GetSection ( modeSec ) ; err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Unknown log mode: %s" , mode )
2014-05-11 22:37:12 +04:00
}
// Log level.
2014-05-19 06:05:35 +04:00
levelName := Cfg . MustValueRange ( "log." + mode , "LEVEL" , "Trace" ,
[ ] string { "Trace" , "Debug" , "Info" , "Warn" , "Error" , "Critical" } )
2014-05-11 22:37:12 +04:00
level , ok := logLevels [ levelName ]
if ! ok {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Unknown log level: %s" , levelName )
2014-05-11 22:37:12 +04:00
}
// Generate log configuration.
switch mode {
case "console" :
LogConfigs [ i ] = fmt . Sprintf ( ` { "level":%s} ` , level )
case "file" :
2014-05-28 09:53:06 +04:00
logPath := Cfg . MustValue ( modeSec , "FILE_NAME" , path . Join ( LogRootPath , "gogs.log" ) )
2014-05-11 22:37:12 +04:00
os . MkdirAll ( path . Dir ( logPath ) , os . ModePerm )
LogConfigs [ i ] = fmt . Sprintf (
` { "level":%s,"filename":"%s","rotate":%v,"maxlines":%d,"maxsize":%d,"daily":%v,"maxdays":%d} ` , level ,
logPath ,
Cfg . MustBool ( modeSec , "LOG_ROTATE" , true ) ,
Cfg . MustInt ( modeSec , "MAX_LINES" , 1000000 ) ,
1 << uint ( Cfg . MustInt ( modeSec , "MAX_SIZE_SHIFT" , 28 ) ) ,
Cfg . MustBool ( modeSec , "DAILY_ROTATE" , true ) ,
Cfg . MustInt ( modeSec , "MAX_DAYS" , 7 ) )
case "conn" :
2014-06-20 08:25:23 +04:00
LogConfigs [ i ] = fmt . Sprintf ( ` { "level":%s,"reconnectOnMsg":%v,"reconnect":%v,"net":"%s","addr":"%s"} ` , level ,
2014-05-26 04:11:25 +04:00
Cfg . MustBool ( modeSec , "RECONNECT_ON_MSG" ) ,
Cfg . MustBool ( modeSec , "RECONNECT" ) ,
2014-05-19 06:05:35 +04:00
Cfg . MustValueRange ( modeSec , "PROTOCOL" , "tcp" , [ ] string { "tcp" , "unix" , "udp" } ) ,
2014-05-11 22:37:12 +04:00
Cfg . MustValue ( modeSec , "ADDR" , ":7020" ) )
case "smtp" :
2014-06-20 08:25:23 +04:00
LogConfigs [ i ] = fmt . Sprintf ( ` { "level":%s,"username":"%s","password":"%s","host":"%s","sendTos":"%s","subject":"%s"} ` , level ,
2014-05-11 22:37:12 +04:00
Cfg . MustValue ( modeSec , "USER" , "example@example.com" ) ,
Cfg . MustValue ( modeSec , "PASSWD" , "******" ) ,
Cfg . MustValue ( modeSec , "HOST" , "127.0.0.1:25" ) ,
Cfg . MustValue ( modeSec , "RECEIVERS" , "[]" ) ,
Cfg . MustValue ( modeSec , "SUBJECT" , "Diagnostic message from serve" ) )
case "database" :
2014-06-20 08:25:23 +04:00
LogConfigs [ i ] = fmt . Sprintf ( ` { "level":%s,"driver":"%s","conn":"%s"} ` , level ,
2014-05-19 06:05:35 +04:00
Cfg . MustValue ( modeSec , "DRIVER" ) ,
2014-05-11 22:37:12 +04:00
Cfg . MustValue ( modeSec , "CONN" ) )
}
log . NewLogger ( Cfg . MustInt64 ( "log" , "BUFFER_LEN" , 10000 ) , mode , LogConfigs [ i ] )
log . Info ( "Log Mode: %s(%s)" , strings . Title ( mode ) , levelName )
2014-04-10 22:20:58 +04:00
}
}
func newCacheService ( ) {
2014-05-19 06:05:35 +04:00
CacheAdapter = Cfg . MustValueRange ( "cache" , "ADAPTER" , "memory" , [ ] string { "memory" , "redis" , "memcache" } )
2014-04-21 14:54:07 +04:00
if EnableRedis {
2014-04-20 06:13:22 +04:00
log . Info ( "Redis Enabled" )
}
2014-04-21 14:54:07 +04:00
if EnableMemcache {
2014-04-20 06:13:22 +04:00
log . Info ( "Memcache Enabled" )
}
2014-04-10 22:20:58 +04:00
switch CacheAdapter {
case "memory" :
2014-08-01 01:25:34 +04:00
CacheInternal = Cfg . MustInt ( "cache" , "INTERVAL" , 60 )
2014-04-10 22:20:58 +04:00
case "redis" , "memcache" :
2014-08-01 01:25:34 +04:00
CacheConn = strings . Trim ( Cfg . MustValue ( "cache" , "HOST" ) , "\" " )
2014-04-10 22:20:58 +04:00
default :
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Unknown cache adapter: %s" , CacheAdapter )
2014-04-10 22:20:58 +04:00
}
log . Info ( "Cache Service Enabled" )
}
func newSessionService ( ) {
2014-05-19 06:05:35 +04:00
SessionProvider = Cfg . MustValueRange ( "session" , "PROVIDER" , "memory" ,
[ ] string { "memory" , "file" , "redis" , "mysql" } )
2014-04-10 22:20:58 +04:00
SessionConfig = new ( session . Config )
2014-07-26 08:24:27 +04:00
SessionConfig . ProviderConfig = strings . Trim ( Cfg . MustValue ( "session" , "PROVIDER_CONFIG" ) , "\" " )
2014-04-10 22:20:58 +04:00
SessionConfig . CookieName = Cfg . MustValue ( "session" , "COOKIE_NAME" , "i_like_gogits" )
2014-07-26 08:24:27 +04:00
SessionConfig . Secure = Cfg . MustBool ( "session" , "COOKIE_SECURE" )
2014-04-10 22:20:58 +04:00
SessionConfig . EnableSetCookie = Cfg . MustBool ( "session" , "ENABLE_SET_COOKIE" , true )
2014-07-26 08:24:27 +04:00
SessionConfig . Gclifetime = Cfg . MustInt64 ( "session" , "GC_INTERVAL_TIME" , 86400 )
SessionConfig . Maxlifetime = Cfg . MustInt64 ( "session" , "SESSION_LIFE_TIME" , 86400 )
2014-05-19 06:05:35 +04:00
SessionConfig . SessionIDHashFunc = Cfg . MustValueRange ( "session" , "SESSION_ID_HASHFUNC" ,
"sha1" , [ ] string { "sha1" , "sha256" , "md5" } )
2014-08-30 16:49:51 +04:00
SessionConfig . SessionIDHashKey = Cfg . MustValue ( "session" , "SESSION_ID_HASHKEY" , string ( com . RandomCreateBytes ( 16 ) ) )
2014-04-10 22:20:58 +04:00
if SessionProvider == "file" {
os . MkdirAll ( path . Dir ( SessionConfig . ProviderConfig ) , os . ModePerm )
}
log . Info ( "Session Service Enabled" )
}
2014-05-26 04:11:25 +04:00
// Mailer represents mail service.
type Mailer struct {
Name string
Host string
2014-06-03 20:51:53 +04:00
From string
2014-05-26 04:11:25 +04:00
User , Passwd string
}
type OauthInfo struct {
ClientId , ClientSecret string
Scopes string
AuthUrl , TokenUrl string
}
// Oauther represents oauth service.
type Oauther struct {
GitHub , Google , Tencent ,
Twitter , Weibo bool
OauthInfos map [ string ] * OauthInfo
}
var (
MailService * Mailer
OauthService * Oauther
)
2014-04-10 22:20:58 +04:00
func newMailService ( ) {
// Check mailer setting.
if ! Cfg . MustBool ( "mailer" , "ENABLED" ) {
return
}
MailService = & Mailer {
Name : Cfg . MustValue ( "mailer" , "NAME" , AppName ) ,
Host : Cfg . MustValue ( "mailer" , "HOST" ) ,
User : Cfg . MustValue ( "mailer" , "USER" ) ,
Passwd : Cfg . MustValue ( "mailer" , "PASSWD" ) ,
}
2014-06-03 20:51:53 +04:00
MailService . From = Cfg . MustValue ( "mailer" , "FROM" , MailService . User )
2014-04-10 22:20:58 +04:00
log . Info ( "Mail Service Enabled" )
}
func newRegisterMailService ( ) {
if ! Cfg . MustBool ( "service" , "REGISTER_EMAIL_CONFIRM" ) {
return
} else if MailService == nil {
log . Warn ( "Register Mail Service: Mail Service is not enabled" )
return
}
Service . RegisterEmailConfirm = true
log . Info ( "Register Mail Service Enabled" )
}
func newNotifyMailService ( ) {
if ! Cfg . MustBool ( "service" , "ENABLE_NOTIFY_MAIL" ) {
return
} else if MailService == nil {
log . Warn ( "Notify Mail Service: Mail Service is not enabled" )
return
}
2014-06-21 08:51:41 +04:00
Service . EnableNotifyMail = true
2014-04-10 22:20:58 +04:00
log . Info ( "Notify Mail Service Enabled" )
}
2014-06-08 12:45:34 +04:00
func newWebhookService ( ) {
WebhookTaskInterval = Cfg . MustInt ( "webhook" , "TASK_INTERVAL" , 1 )
WebhookDeliverTimeout = Cfg . MustInt ( "webhook" , "DELIVER_TIMEOUT" , 5 )
}
2014-05-26 04:11:25 +04:00
func NewServices ( ) {
2014-04-10 22:20:58 +04:00
newService ( )
newLogService ( )
newCacheService ( )
newSessionService ( )
newMailService ( )
newRegisterMailService ( )
newNotifyMailService ( )
2014-06-08 12:45:34 +04:00
newWebhookService ( )
2014-07-26 08:24:27 +04:00
// ssh.Listen("2022")
2014-04-10 22:20:58 +04:00
}