Compare commits

...

123 Commits

Author SHA1 Message Date
Unknwon
d5ad4e1141 prepare release 2015-09-26 14:09:09 -04:00
Unknwon
2dde2a8ad3 fix error detect when push commits 2015-09-25 21:06:31 -04:00
Unknwon
6dfee30bf0 #1602 Wrong commit order on issue page when pushing multiple commits 2015-09-25 20:35:56 -04:00
Unknwon
21e13cb51e #1525 Triggere mailer for admin created accounts 2015-09-25 19:45:44 -04:00
Unknwon
986447335d #1693 minor fix 2015-09-25 19:07:21 -04:00
无闻
84c727ae66 Merge pull request #1693 from sapk/fix-admin-repo-new-ui
Fix admin repos new ui
2015-09-25 19:05:27 -04:00
无闻
ee1256cf74 Merge pull request #1691 from sapk/fix-admin-organization-new-ui
Fix admin organization new ui
2015-09-25 19:03:53 -04:00
Antoine GIRARD
dfc16d0879 Fix admin repos new ui 2015-09-26 00:38:43 +02:00
Antoine GIRARD
a8fd615adc Fix indent in notice router 2015-09-25 19:56:23 +02:00
Antoine GIRARD
c98dad1cf3 New admin organization UI 2015-09-25 19:54:52 +02:00
Unknwon
0d5e57e4ae #1689 minor fixes and update locale 2015-09-25 12:58:39 -04:00
无闻
1c35380c2c Merge pull request #1689 from sapk/fix-admin-notice-new-ui
New admin notice UI based on user list example
2015-09-25 12:53:18 -04:00
Antoine GIRARD
3544dafb64 Fix indent 2015-09-25 18:39:31 +02:00
Antoine GIRARD
fefce965f9 Add total next to system notice 2015-09-25 18:36:05 +02:00
Antoine GIRARD
14a1101139 New admin notice UI based on user list example 2015-09-25 18:13:38 +02:00
无闻
4f8b209956 Merge pull request #1678 from TheHowl/TheHowl/fix-go-get-not-working-with-user-or-repo-names-with-uppercase-letters
Fix `go get` not working when there is a username or repository name with some uppercase letters.
2015-09-24 18:51:02 -04:00
Unknwon
043ded0896 fix #1683 2015-09-24 18:42:29 -04:00
Unknwon
e07675b480 fix test custom import path 2015-09-24 18:03:08 -04:00
Unknwon
4c30caad1c fix test import path 2015-09-24 17:58:40 -04:00
Unknwon
216f0477b5 fix subrepo break change 2015-09-24 17:55:01 -04:00
无闻
befed9c20c Merge pull request #1662 from SergioBenitez/develop
Commit messages now rendered with line breaks and prefix spacing.
2015-09-24 17:23:53 -04:00
Unknwon
e787e73e2f fix URL match 2015-09-24 14:20:07 -04:00
Howl
f8c09dc1ff Fix go get not working when there is a username or repository name with some uppercase letters. 2015-09-23 16:16:52 +02:00
Unknwon
42a38dfca3 #1665 improve docs 2015-09-21 10:44:40 -04:00
Unknwon
91220a2501 #1665 improve Docker docs 2015-09-21 10:13:47 -04:00
Unknwon
700ac8dea7 fix #1664 2015-09-21 10:09:21 -04:00
Unknwon
69b1d65c9b remove unused file and update locale 2015-09-20 23:29:59 -04:00
Sergio Benitez
7acbcf9ddd Commit messages now rendered with line breaks and prefix spacing. 2015-09-18 19:02:23 -07:00
Unknwon
2340bb1ed2 update locales 2015-09-18 10:02:17 -04:00
Unknwon
d9c5b3bcee #1659 fix change label title resets color 2015-09-18 06:18:31 -04:00
Unknwon
86d3c5cbb3 update Docker info 2015-09-17 19:18:49 -04:00
Unknwon
acf428863c fix #981 2015-09-17 17:21:27 -04:00
Unknwon
3fb1b6a608 drop oauth2 feature support 2015-09-17 16:11:44 -04:00
Unknwon
562e47f31c finish all new template for current ones 2015-09-17 15:45:36 -04:00
Unknwon
9d36fc6986 finish new auth e-mails 2015-09-17 14:57:24 -04:00
Unknwon
923873db85 fix possible XSS in view issue page 2015-09-17 14:22:46 -04:00
Unknwon
373731f5e8 user gomail and new activate account email tpl
- #1496: fallback plain text
- #1002: add date header
- #913: fix encoding of header
2015-09-17 01:54:12 -04:00
Unknwon
e75fd2f783 simplify names 2015-09-16 23:08:46 -04:00
Unknwon
a517cfdf7b fix codekit 2015-09-16 15:36:21 -04:00
Unknwon
2729eb998c #1646 and other minor fixes 2015-09-16 15:34:46 -04:00
Unknwon
b003b18788 #1649 Using commas to delineate emails 2015-09-16 15:04:54 -04:00
Unknwon
6a1907d994 revert simple LDAP userDN and update example 2015-09-16 12:15:14 -04:00
Unknwon
e303d74ab6 #1643 fix delete milestone 2015-09-15 10:03:50 -04:00
Unknwon
e4ecbcdf4a new sign in page 2015-09-14 22:50:44 -04:00
Unknwon
f5c7f22cc8 #1637 able to skip verify for LDAP 2015-09-14 15:48:51 -04:00
Unknwon
2bc3e83e1c fix simple LDAP userDN 2015-09-14 12:24:37 -04:00
Unknwon
d600530c20 #1635 PAM return error bug 2015-09-14 11:03:42 -04:00
Unknwon
2d1bb0cf49 #1633 admin delete user 2015-09-13 13:26:20 -04:00
Unknwon
5f1183cecf #697 add install option 2015-09-13 12:14:32 -04:00
Unknwon
b0bf4cc1cb fix #1632 and #1606 2015-09-13 11:26:25 -04:00
Unknwon
98108e379d #697 update locale and admin panel 2015-09-13 11:16:29 -04:00
Unknwon
83e747bfda #697 and #1606 and new admin edit user UI 2015-09-13 11:07:21 -04:00
Unknwon
e5ed5904c6 #1606 GUI bug while adding ldap user 2015-09-13 10:05:18 -04:00
Unknwon
1fa5b6711b fix html logic 2015-09-13 09:56:50 -04:00
Unknwon
8e0a69f86a #697 disable captcha and new admin create user UI 2015-09-13 09:51:51 -04:00
Unknwon
e2d6b0116e #1500 only regulateTimeZone for MySQL 2015-09-13 07:32:00 -04:00
Unknwon
cd37fccdfb #1625 remove auto_register and makes it default 2015-09-12 20:58:51 -04:00
Unknwon
6969c20afd #1625 LDAP not working 2015-09-12 19:20:37 -04:00
Unknwon
f4e54aafa5 typo in logo 2015-09-12 17:01:10 -04:00
Unknwon
d185f601d3 #1622 comment with whitespace 2015-09-12 16:58:18 -04:00
Unknwon
aff773f1b9 add tidb support 2015-09-12 15:31:36 -04:00
Unknwon
10de16beb0 typo and README 2015-09-12 09:21:09 -04:00
Unknwon
ed5a61153f fix #1629 2015-09-12 07:17:37 -04:00
Unknwon
47df562ced #1626 default db_type 2015-09-11 21:00:12 -04:00
Unknwon
c7ac237b57 fix css 2015-09-11 20:49:28 -04:00
Unknwon
be89802bd8 new admin user list UI 2015-09-11 20:42:26 -04:00
Unknwon
247017d9ff clean log 2015-09-11 13:33:36 -04:00
Unknwon
362d64df04 #1620 add allowed domains for SMTP auth 2015-09-11 13:32:33 -04:00
Unknwon
373ef5d15e #1500 timezone minutes offset 2015-09-11 12:33:27 -04:00
Unknwon
121a81a2c5 finish new edit auth UI 2015-09-11 12:03:08 -04:00
Unknwon
0617448282 finish new add auth UI 2015-09-10 17:11:41 -04:00
Unknwon
fa728d8dff more minor fix on 1581 2015-09-10 15:45:03 -04:00
Unknwon
26ac016b9f minor fix on #1581 2015-09-10 15:03:14 -04:00
Unknwon
cbd6276200 Merge branch 'develop' of https://github.com/SergioBenitez/gogs into develop
# Conflicts:
#	modules/bindata/bindata.go
2015-09-10 14:55:29 -04:00
Unknwon
52ec80fa18 finish all new user settings UI 2015-09-10 11:40:34 -04:00
Unknwon
c8d92fad30 #1595 pushing new branch will rereference issues in previous branch 2015-09-10 07:53:40 -04:00
无闻
c3061c61a7 Merge pull request #1609 from denghongcai/develop
Add allow attribute `class` to `<code>` tag
2015-09-10 05:51:17 -04:00
Hongcai Deng
e4d4662074 add regexp to restrict <code class=""></code> 2015-09-10 09:06:09 +08:00
Unknwon
9899ea71e8 #1611 fix bool type in sqlite query 2015-09-09 18:07:10 -04:00
Sergio Benitez
b954a22ce2 Updated binary data. 2015-09-09 13:45:38 -07:00
Sergio Benitez
964d0262ff Merged conflicts. 2015-09-09 13:44:51 -07:00
Unknwon
b75d0378cb clean log 2015-09-09 12:42:24 -04:00
Hongcai Deng
357c002c03 add allow attribute class to <code> 2015-09-09 17:31:14 +08:00
Unknwon
f432f1f41c #1604 suburl for release md preview 2015-09-08 10:05:57 -04:00
Unknwon
6f6f38e7c3 #1603 suburl for comment attachments 2015-09-08 09:09:40 -04:00
Unknwon
49e120a67c #1602 change status after comment issue 2015-09-08 06:34:02 -04:00
Unknwon
af324a6165 adjust icon css 2015-09-07 16:12:58 -04:00
Unknwon
7b0ae27549 #1599 org dashboard icon css 2015-09-07 15:37:34 -04:00
Unknwon
31e7b0f588 #1599 status button mrigin-right 2015-09-07 15:22:52 -04:00
Unknwon
03ea29eb36 minor fix on icon 2015-09-07 15:03:31 -04:00
Unknwon
32c12d553c Able to use GOGS_WORK_DIR environment variable to specify work directory 2015-09-07 14:06:05 -04:00
Unknwon
cc83043edc minor fix on template 2015-09-07 14:02:09 -04:00
Unknwon
3d9b98fae4 #1585 order owners list by last changed time 2015-09-07 13:58:23 -04:00
无闻
36405d0faa Merge pull request #1596 from denghongcai/develop
implemented #1528
2015-09-07 13:10:59 -04:00
Unknwon
5020576e80 minor regexp fix on #1593 2015-09-07 11:56:06 -04:00
Unknwon
56eb252098 #1593 fix repo name router bug 2015-09-07 11:51:53 -04:00
Hongcai Deng
fbb3486c95 fix syntax error 2015-09-07 18:28:56 +08:00
Hongcai Deng
7b92dc3d9d implemented #1528 2015-09-07 18:25:15 +08:00
Unknwon
645d4d0c5b #1594 Collaborative Repositories missing suburl on Dashboard 2015-09-07 05:57:19 -04:00
Unknwon
232c22208c remove rsync 2015-09-06 18:53:59 -04:00
Unknwon
0806725ca5 minor fix on #1589
Signed-off-by: Unknwon <u@gogs.io>
2015-09-06 18:19:30 -04:00
无闻
d504ee0417 Merge pull request #1589 from siadat/migrating-empty-repository
Set IsBare true if migrated repo is empty
2015-09-06 18:17:58 -04:00
Sina Siadat
ae1650824c Set IsBare true if migrated repo is empty.
Executes `git log -1` and sets IsBare to true if stderr contains this
string:

  fatal: bad default revision 'HEAD'
2015-09-07 02:20:51 +04:30
Unknwon
c38754d432 #1191 allow upload avatar for org
Signed-off-by: Unknwon <u@gogs.io>
2015-09-06 17:12:02 -04:00
Unknwon
85f34ba538 new user profile settings UI
Signed-off-by: Unknwon <u@gogs.io>
2015-09-06 16:31:22 -04:00
Unknwon
00767a0522 finish new org settings page 2015-09-06 11:25:08 -04:00
Unknwon
4dbc322859 fix org header order 2015-09-06 11:01:05 -04:00
Unknwon
edc99bc8a4 new org options UI 2015-09-06 10:56:31 -04:00
Unknwon
80701d45bb #1193 Make organization emails non-mandatory 2015-09-06 10:08:14 -04:00
Unknwon
aa67de910a fix #1558 2015-09-06 08:54:08 -04:00
Unknwon
24658fcbdd #633 dix suburl 2015-09-06 06:57:25 -04:00
Unknwon
705224353b minor fix 2015-09-05 19:58:44 -04:00
Sergio Benitez
079a2d68db Minor fixes to the LDAP module readme 2015-09-05 15:26:31 -07:00
Unknwon
119dec51f2 Mirror repository should not allow fork 2015-09-05 14:31:52 -04:00
Unknwon
46dce2d653 fix emoji edit not rendered 2015-09-05 14:18:02 -04:00
Unknwon
d48cde6ec8 #633 emoji 2015-09-05 14:14:03 -04:00
Unknwon
3b0c2cb480 #633 Add Emoji support 2015-09-05 14:11:05 -04:00
无闻
d464d3b0c3 Merge pull request #1584 from manfer/dashboardtableheader
remove dashboard operations table header
2015-09-05 11:33:27 -04:00
Gogs
b80aef0fa6 remove dashboard operations table header 2015-09-05 16:57:25 +02:00
Sergio Benitez
0f07a5cb84 Updated the LDAP auth module README. 2015-09-04 21:14:20 -07:00
Sergio Benitez
7cb4aa8d82 Updated binary data. 2015-09-04 20:52:35 -07:00
Sergio Benitez
aa9c36514f Merged conflicts. 2015-09-04 20:49:07 -07:00
Sergio Benitez
2d1db4bf05 Added LDAP simple auth support. 2015-09-04 20:48:34 -07:00
1092 changed files with 19739 additions and 4283 deletions

View File

@@ -13,8 +13,7 @@ watch_dirs = [
watch_exts = [".go"]
build_delay = 1500
cmds = [
#["go-bindata", "-o=modules/bindata/bindata.go", "-ignore=\\.DS_Store|README", "-pkg=bindata", "conf/..."],
["go", "install", "-tags", "sqlite"],# redis memcache cert pam
["go", "build", "-tags", "sqlite"],
["go", "install", "-tags", "sqlite tidb"],# redis memcache cert pam
["go", "build", "-tags", "sqlite tidb"],
["./gogs", "web"]
]

View File

@@ -6,26 +6,25 @@ github.com/bradfitz/gomemcache = commit:72a68649ba
github.com/Unknwon/cae = commit:2e70a1351b
github.com/Unknwon/com = commit:47d7d2b81a
github.com/Unknwon/i18n = commit:7457d88830
github.com/Unknwon/macaron = commit:635c89ac74
github.com/Unknwon/macaron = commit:05317cffe5
github.com/Unknwon/paginater = commit:cab2d086fa
github.com/codegangsta/cli = commit:142e6cd241
github.com/go-sql-driver/mysql = commit:3dd7008ac1
github.com/go-xorm/core = commit:515edd92c1
github.com/go-xorm/xorm = commit:ce23797899
github.com/go-sql-driver/mysql = commit:527bcd55aa
github.com/go-xorm/core = commit:3e10003353
github.com/go-xorm/xorm = commit:803f6db50c
github.com/gogits/chardet = commit:2404f77725
github.com/gogits/go-gogs-client = commit:519eee0af0
github.com/lib/pq = commit:b269bd035a
github.com/macaron-contrib/binding = commit:de6ed78668
github.com/macaron-contrib/binding = commit:1935a991f2
github.com/macaron-contrib/cache = commit:a139ea1eee
github.com/macaron-contrib/captcha = commit:9a0a0b1468
github.com/macaron-contrib/csrf = commit:98ddf5a710
github.com/macaron-contrib/i18n = commit:da2b19e90b
github.com/macaron-contrib/oauth2 = commit:1adb5ce072
github.com/macaron-contrib/session = commit:e48134e803
github.com/macaron-contrib/toolbox = commit:acbfe36e16
github.com/mattn/go-sqlite3 = commit:897b8800a7
github.com/mattn/go-sqlite3 = commit:b808f01f66
github.com/mcuadros/go-version = commit:d52711f8d6
github.com/microcosm-cc/bluemonday = commit:2b7763a06c
github.com/microcosm-cc/bluemonday = commit:85ba47ef2c
github.com/mssola/user_agent = commit:a163d6a569
github.com/msteinert/pam = commit:6534f23b39
github.com/nfnt/resize = commit:dc93e1b98c
@@ -33,7 +32,8 @@ github.com/russross/blackfriday = commit:8cec3a854e
github.com/shurcooL/sanitized_anchor_name = commit:244f5ac324
golang.org/x/net =
golang.org/x/text =
gopkg.in/ini.v1 = commit:463307112d
gopkg.in/gomail.v2 = commit:b1e55520bf
gopkg.in/ini.v1 = commit:e8c222fea7
gopkg.in/redis.v2 = commit:e617904962
[res]

View File

@@ -5,7 +5,7 @@ RUN echo "deb http://ftp.debian.org/debian/ wheezy-backports main" >> /etc/apt/s
apt-get update -qqy && \
apt-get install --no-install-recommends -qqy \
curl build-essential ca-certificates git \
openssh-server rsync libpam-dev && \
openssh-server libpam-dev && \
apt-get autoclean && \
apt-get autoremove && \
rm -rf /var/lib/apt/lists/*

View File

@@ -3,9 +3,9 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gogits/gogs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Gogs (Go Git Service) is a painless self-hosted Git service.
![](public/img/gogs-large-resize.png)
##### Current version: 0.6.9 Beta
##### Current version: 0.6.15 Beta
<table>
<tr>
@@ -61,8 +61,8 @@ The goal of this project is to make the easiest, fastest, and most painless way
- Gravatar and custom source support
- Mail service
- Administration panel
- Supports MySQL, PostgreSQL and SQLite3
- Social account login (GitHub, Google, QQ, Weibo)
- CI integration: [Drone](https://github.com/drone/drone)
- Supports MySQL, PostgreSQL, SQLite3 and [TiDB](https://github.com/pingcap/tidb)
- Multi-language support ([14 languages](https://crowdin.com/project/gogs))
## System Requirements
@@ -99,6 +99,12 @@ There are 5 ways to install Gogs:
- [Instalando Gogs no Ubuntu](http://blog.linuxpro.com.br/2015/08/14/instalando-gogs-no-ubuntu/) (Português)
### Deploy to Cloud
- [OpenShift](https://github.com/tkisme/gogs-openshift)
- [Cloudron](https://cloudron.io/appstore.html#io.gogs.cloudronapp)
- [Scaleway](https://www.scaleway.com/imagehub/gogs/)
## Acknowledgments
- Router and middleware mechanism of [Macaron](https://github.com/Unknwon/macaron).

View File

@@ -28,8 +28,8 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
- 支持 Gravatar 以及自定义源
- 支持邮件服务
- 支持后台管理面板
- 支持 MySQL、PostgreSQL 以及 SQLite3 数据库
- 支持社交帐号登录GitHub、Google、QQ、微博
- 支持 CI 集成:[Drone](https://github.com/drone/drone)
- 支持 MySQL、PostgreSQL、SQLite3 和 [TiDB](https://github.com/pingcap/tidb) 数据库
- 支持多语言本地化([14 种语言]([more](https://crowdin.com/project/gogs))
## 系统要求

View File

@@ -34,8 +34,8 @@ func runDump(ctx *cli.Context) {
if ctx.IsSet("config") {
setting.CustomConf = ctx.String("config")
}
setting.NewConfigContext()
models.LoadModelsConfig()
setting.NewContext()
models.LoadConfigs()
models.SetEngine()
log.Printf("Dumping local repositories...%s", setting.RepoRootPath)

View File

@@ -37,7 +37,7 @@ var CmdServ = cli.Command{
}
func setup(logPath string) {
setting.NewConfigContext()
setting.NewContext()
log.NewGitLogger(filepath.Join(setting.LogRootPath, logPath))
if setting.DisableSSH {
@@ -45,9 +45,9 @@ func setup(logPath string) {
os.Exit(1)
}
models.LoadModelsConfig()
models.LoadConfigs()
if setting.UseSQLite3 {
if setting.UseSQLite3 || setting.UseTiDB {
workDir, _ := setting.WorkDir()
os.Chdir(workDir)
}

View File

@@ -23,7 +23,6 @@ import (
"github.com/macaron-contrib/captcha"
"github.com/macaron-contrib/csrf"
"github.com/macaron-contrib/i18n"
"github.com/macaron-contrib/oauth2"
"github.com/macaron-contrib/session"
"github.com/macaron-contrib/toolbox"
"github.com/mcuadros/go-version"
@@ -167,13 +166,6 @@ func newMacaron() *macaron.Macaron {
},
},
}))
// OAuth 2.
if setting.OauthService != nil {
for _, info := range setting.OauthService.OauthInfos {
m.Use(oauth2.NewOAuth2Provider(info.Options, info.AuthUrl, info.TokenUrl))
}
}
m.Use(middleware.Contexter())
return m
}
@@ -200,7 +192,7 @@ func runWeb(ctx *cli.Context) {
m.Get("/explore", ignSignIn, routers.Explore)
m.Combo("/install", routers.InstallInit).Get(routers.Install).
Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost)
m.Get("/:type(issues|pulls)", reqSignIn, user.Issues)
m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
// ***** START: API *****
// FIXME: custom form error response.
@@ -256,7 +248,6 @@ func runWeb(ctx *cli.Context) {
m.Group("/user", func() {
m.Get("/login", user.SignIn)
m.Post("/login", bindIgnErr(auth.SignInForm{}), user.SignInPost)
m.Get("/info/:name", user.SocialSignIn)
m.Get("/sign_up", user.SignUp)
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
@@ -267,21 +258,20 @@ func runWeb(ctx *cli.Context) {
m.Get("", user.Settings)
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost)
m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), user.SettingsAvatar)
m.Get("/email", user.SettingsEmails)
m.Post("/email", bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost)
m.Combo("/email").Get(user.SettingsEmails).
Post(bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost)
m.Post("/email/delete", user.DeleteEmail)
m.Get("/password", user.SettingsPassword)
m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
m.Combo("/ssh").Get(user.SettingsSSHKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost)
m.Post("/ssh/delete", user.DeleteSSHKey)
m.Get("/social", user.SettingsSocial)
m.Combo("/applications").Get(user.SettingsApplications).
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
m.Post("/applications/delete", user.SettingsDeleteApplication)
m.Route("/delete", "GET,POST", user.SettingsDelete)
}, reqSignIn, func(ctx *middleware.Context) {
ctx.Data["PageIsUserSettings"] = true
ctx.Data["HasOAuthService"] = setting.OauthService != nil
})
m.Group("/user", func() {
@@ -311,7 +301,7 @@ func runWeb(ctx *cli.Context) {
m.Group("/users", func() {
m.Get("", admin.Users)
m.Get("/new", admin.NewUser)
m.Post("/new", bindIgnErr(auth.RegisterForm{}), admin.NewUserPost)
m.Post("/new", bindIgnErr(auth.AdminCrateUserForm{}), admin.NewUserPost)
m.Get("/:userid", admin.EditUser)
m.Post("/:userid", bindIgnErr(auth.AdminEditUserForm{}), admin.EditUserPost)
m.Post("/:userid/delete", admin.DeleteUser)
@@ -329,8 +319,8 @@ func runWeb(ctx *cli.Context) {
m.Get("", admin.Authentications)
m.Get("/new", admin.NewAuthSource)
m.Post("/new", bindIgnErr(auth.AuthenticationForm{}), admin.NewAuthSourcePost)
m.Get("/:authid", admin.EditAuthSource)
m.Post("/:authid", bindIgnErr(auth.AuthenticationForm{}), admin.EditAuthSourcePost)
m.Combo("/:authid").Get(admin.EditAuthSource).
Post(bindIgnErr(auth.AuthenticationForm{}), admin.EditAuthSourcePost)
m.Post("/:authid/delete", admin.DeleteAuthSource)
})
@@ -385,7 +375,7 @@ func runWeb(ctx *cli.Context) {
m.Group("/:org", func() {
m.Get("/dashboard", user.Dashboard)
m.Get("/:type(issues|pulls)", user.Issues)
m.Get("/^:type(issues|pulls)$", user.Issues)
m.Get("/members", org.Members)
m.Get("/members/action/:action", org.MembersAction)
@@ -406,6 +396,7 @@ func runWeb(ctx *cli.Context) {
m.Group("/settings", func() {
m.Combo("").Get(org.Settings).
Post(bindIgnErr(auth.UpdateOrgSettingForm{}), org.SettingsPost)
m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), org.SettingsAvatar)
m.Group("/hooks", func() {
m.Get("", org.Webhooks)
@@ -518,8 +509,8 @@ func runWeb(ctx *cli.Context) {
m.Group("/:username/:reponame", func() {
m.Get("/releases", middleware.RepoRef(), repo.Releases)
m.Get("/:type(issues|pulls)", repo.RetrieveLabels, repo.Issues)
m.Get("/:type(issues|pulls)/:index", repo.ViewIssue)
m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues)
m.Get("/^:type(issues|pulls)$/:index", repo.ViewIssue)
m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
m.Get("/milestones", repo.Milestones)
m.Get("/branches", repo.Branches)
@@ -544,7 +535,7 @@ func runWeb(ctx *cli.Context) {
m.Group("/:username", func() {
m.Group("/:reponame", func() {
m.Get("", repo.Home)
m.Get(".git", repo.Home)
m.Get("\\.git$", repo.Home)
}, ignSignIn, middleware.RepoAssignment(true, true), middleware.RepoRef())
m.Group("/:reponame", func() {

View File

@@ -17,6 +17,18 @@ SCRIPT_TYPE = bash
EXPLORE_PAGING_NUM = 20
; Number of issues that are showed in one page
ISSUE_PAGING_NUM = 10
; Number of maximum commits showed in one activity feed
FEED_MAX_COMMIT_NUM = 5
[ui.admin]
; Number of users that are showed in one page
USER_PAGING_NUM = 50
; Number of repos that are showed in one page
REPO_PAGING_NUM = 50
; Number of notices that are showed in one page
NOTICE_PAGING_NUM = 50
; Number of organization that are showed in one page
ORG_PAGING_NUM = 50
[markdown]
; Enable hard line break extension
@@ -61,7 +73,7 @@ USER = root
PASSWD =
; For "postgres" only, either "disable", "require" or "verify-full"
SSL_MODE = disable
; For "sqlite3" only
; For "sqlite3" and "tidb"
PATH = data/gogs.db
[admin]
@@ -95,6 +107,8 @@ ENABLE_REVERSE_PROXY_AUTHENTICATION = false
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
; Do not check minimum key size with corresponding type
DISABLE_MINIMUM_KEY_SIZE_CHECK = false
; Enable captcha validation for registration
ENABLE_CAPTCHA = true
[webhook]
; Hook task queue length
@@ -109,7 +123,7 @@ PAGING_NUM = 10
[mailer]
ENABLED = false
; Buffer length of channel, keep it as it is if you don't know what it is.
SEND_BUFFER_LEN = 10
SEND_BUFFER_LEN = 100
; Name displayed in mail title
SUBJECT = %(APP_NAME)s
; Mail server
@@ -133,44 +147,6 @@ FROM =
USER =
PASSWD =
[oauth]
ENABLED = false
[oauth.github]
ENABLED = false
CLIENT_ID =
CLIENT_SECRET =
SCOPES = https://api.github.com/user
AUTH_URL = https://github.com/login/oauth/authorize
TOKEN_URL = https://github.com/login/oauth/access_token
; Get client id and secret from
; https://console.developers.google.com/project
[oauth.google]
ENABLED = false
CLIENT_ID =
CLIENT_SECRET =
SCOPES = https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile
AUTH_URL = https://accounts.google.com/o/oauth2/auth
TOKEN_URL = https://accounts.google.com/o/oauth2/token
[oauth.qq]
ENABLED = false
CLIENT_ID =
CLIENT_SECRET =
SCOPES = get_user_info
; QQ 互联
AUTH_URL = https://graph.qq.com/oauth2.0/authorize
TOKEN_URL = https://graph.qq.com/oauth2.0/token
[oauth.weibo]
ENABLED = false
CLIENT_ID =
CLIENT_SECRET =
SCOPES = all
AUTH_URL = https://api.weibo.com/oauth2/authorize
TOKEN_URL = https://api.weibo.com/oauth2/access_token
[cache]
; Either "memory", "redis", or "memcache", default is "memory"
ADAPTER = memory

View File

@@ -5,7 +5,6 @@ dashboard=Табло
explore=Разгледай
help=Помощ
sign_in=Влизане
social_sign_in=Вход чрез социална мрежа: 2. стъпка <small>асоцииране на профил</small>
sign_out=Излизане
sign_up=Регистрирайте се
register=Регистриране
@@ -21,7 +20,7 @@ signed_in_as=Вписан като
username=Потребител
email=Ел. поща
password=Парола
re_type=Въведете отново
re_type=Въведете повторно
captcha=Captcha
repository=Хранилище
@@ -54,7 +53,8 @@ code=Код
[install]
install=Инсталация
title=Стъпки за инсталиране при първоначално стартиране
requite_db_desc=Gogs изисква MySQL, PostgreSQL или SQLite3.
docker_helper=Ако Gogs е стартиран в Docker контейнер, моля прочетете <a target="_blank" href="%s">нашите указания</a> внимателно, преди да правите промени по настройките на тази страница!
requite_db_desc=Gogs изисква MySQL, PostgreSQL, SQLite3 или TiDB.
db_title=Настройки на базата данни
db_type=Тип на база данни
host=Сървър
@@ -64,8 +64,11 @@ db_name=Име на база данни
db_helper=Моля, използвайте INNODB engine с utf8_general_ci кодиране на знаци за MySQL.
ssl_mode=Режим SSL
path=Път
sqlite_helper=Пък към файла на SQLite3 база данни.
err_empty_sqlite_path=Пътят за SQLite3 база за данни не може да е празен.
sqlite_helper=Файл на SQLite3 или TiDB база данни.
err_empty_db_path=Пътят до SQLite3 или TiDB база данни не може да е празен.
err_invalid_tidb_name=TiDB не позволява "." и "-" в името на базата данни.
no_admin_and_disable_registration=Невъзможно изключване на регистрациите без предварително да е създаден поне един административен профил.
err_empty_admin_password=Паролата на администратор не може да е празна.
general_title=Общи настройки на приложението
app_name=Име на приложението
@@ -73,11 +76,11 @@ app_name_helper=Постави името на твоята организаци
repo_path=Основен път към хранилищата
repo_path_helper=Всички отдалечени хранилища на Git ще бъдат съхранени в тази директория.
run_user=Потребителски контекст
run_user_helper=Този потребител трябва да има достъп до основния път до хранилищата и права да стартира Gogs.
run_user_helper=Този потребител трябва да има достъп до основния път към хранилищата и права да стартира Gogs.
domain=Домейн
domain_helper=Тази настройка влияе на URL адреса за клониране със SSH.
domain_helper=Тази настройка влияе на URL адреса за клониране чрез SSH.
ssh_port=SSH порт
ssh_port_helper=Номер на порт на SSH сървъра. Оставете празно за да забраните SSH.
ssh_port_helper=Номер на порт на SSH сървъра. Оставете празно за да изключите достъп през SSH.
http_port=HTTP порт
http_port_helper=Порт, на който приложението ще слуша.
app_url=URL адрес на приложението
@@ -87,7 +90,7 @@ optional_title=Опционални настройки
email_title=Настройки на пощенска услуга
smtp_host=SMTP сървър
smtp_from=Подател
smtp_from_helper=Адрес на подател на поща по RFC 5322. Може да бъде обикновен адрес на ел. поща или на във формат "Име" <email@example.com>.
smtp_from_helper=Адрес на подател на поща по RFC 5322. Може да бъде обикновен адрес на ел. поща или във формат "Име" <email@example.com>.
mailer_user=Ел. поща за изпращане
mailer_password=Парола за изпращане
register_confirm=Включи потвърждението на регистрациите
@@ -99,23 +102,25 @@ disable_gravatar=Изключи връзка с Gravatar
disable_gravatar_popup=Изключва Gravatar и външни източници, така че всички аватари трябва да са или качени от потребителите или да се ползват аватари по подразбиране.
disable_registration=Изключи саморегистрацията
disable_registration_popup=Изключи потребителската саморегистрация, само администратор може да създава профили.
enable_captcha=Включи Captcha
enable_captcha_popup=Изисква валидиране с captcha при саморегистрация на потребители.
require_sign_in_view=Включи задължително вписване за преглед на страници
require_sign_in_view_popup=Само вписани потребители могат да виждат страниците, посетителите виждат само страниците за регистрация и вход.
admin_setting_desc=Няма нужда да създавате администраторски профил в момента, защото потребителят с първо ID в базата автоматично получава администраторски достъп.
require_sign_in_view_popup=Само вписани потребители могат да виждат страниците, анонимните посетители виждат само страниците за регистрация и вход.
admin_setting_desc=Няма нужда от създаване на администраторски профил в момента, защото потребителят с първо ID в базата автоматично получава администраторски достъп.
admin_title=Настройки на профил на администратора
admin_name=Потребителско име
admin_password=Парола
confirm_password=Потвърждение на паролата
admin_email=Ел. поща
install_gogs=Инсталирай Gogs
test_git_failed=Неуспех при тестването на "git" команда: %v
test_git_failed=Неуспешно тестването на "git" команда: %v
sqlite3_not_available=Вашата версия не поддържа SQLite3, моля, изтеглете официалната двоична версия от %s, а не gobuild версията.
invalid_db_setting=Настройките на базата данни са некоректни: %v
invalid_repo_path=Основният път към хранилищата е невалиден: %v
run_user_not_match=Потребителският контекст на приложението не е на текущия потребител: %s -> %s
save_config_failed=Неуспех при запазване на конфигурация: %v
save_config_failed=Неуспешно запазване на конфигурация: %v
invalid_admin_setting=Настройките на профил на администратора са невалидни: %v
install_success=Добре дошли! Радваме се, че избрахте Gogs и Ви пожелаваме приятна работа и сърдечни поздрави!
install_success=Добре дошли! Радваме се, че избрахте Gogs, и Ви пожелаваме приятна работа и сърдечни поздрави!
[home]
uname_holder=Потребителско име или ел. поща
@@ -127,7 +132,7 @@ my_orgs=Моите организации
my_mirrors=Моите огледала
view_home=Преглед на %s
issues.in_your_repos=Във вашите хранилища
issues.in_your_repos=Във Вашите хранилища
[explore]
repos=Хранилища
@@ -136,24 +141,30 @@ repos=Хранилища
create_new_account=Създай нов профил
register_hepler_msg=Вече имате профил? Впишете се сега!
social_register_hepler_msg=Вече имате профил? Свържете се сега!
disable_register_prompt=За съжаление новите регистрации са изключени. Обърнете се към администратора на сайта.
disable_register_mail=За съжаление потвърждението на регистрациите е изключено.
disable_register_prompt=За съжаление създаването на нови регистрации е изключено. Обърнете се към администратора на сайта.
disable_register_mail=За съжаление потвърждението на регистрации е изключено.
remember_me=Запомни ме
forgot_password=Забравена парола
forget_password=Забравена парола?
sign_up_now=Нуждаете се от профил? Регистрирайте се сега.
confirmation_mail_sent_prompt=Ново писмо за потвърждение е изпратено до <b>%s</b>. Моля проверете пощенската си кутия в рамките на следващите %d часа, за да завършите процеса на регистрация.
sign_in_email=Влизане чрез ел. поща
sign_in_to_account=Влезте с Вашия профил
active_your_account=Активиране на профил
resent_limit_prompt=За съжаление Вие съвсем наскоро изпратихте писмо за активация. Моля изчакайте 3 минути, след което опитайте отново.
has_unconfirmed_mail=Здравейте %s, имате непотвърден адрес на ел. поща (<b>%s</b>). Ако не сте получили писмо за потвърждение или имате нужда да се изпрати ново, моля щракнете бутона по-долу.
resend_mail=Щракнете тук, за да се изпрати ново писмо за активиране
has_unconfirmed_mail=Здравейте %s, имате непотвърден адрес на ел. поща (<b>%s</b>). Ако не сте получили писмо за потвърждение или имате нужда да се изпрати ново писмо, моля щракнете бутона по-долу.
resend_mail=Щракнете тук, за да се изпрати ново писмо за потвърждение
email_not_associate=Този адрес на ел. поща не е свързан с никой профил.
send_reset_mail=Щракнете тук, за да получите (наново) писмо за нулиране на паролата
send_reset_mail=Щракнете тук, за да получите (отново) писмо за нулиране на паролата
reset_password=Нулиране на паролата
invalid_code=За съжаление Вашия код за потвърждение е изтекъл или е невалиден.
reset_password_helper=Щракнете тук, за да нулирате паролата си
password_too_short=Дължина на паролата не може да бъде по-малко от 6 знака.
password_too_short=Размерът на паролата не може да бъде по-малък от 6 знака.
[mail]
activate_account=Моля активирайте Вашия профил
activate_email=Провери адрес на ел. поща
reset_password=Нулиране на паролата
register_success=Успешна регистрация и добре дошли
[modal]
yes=Да
@@ -162,15 +173,15 @@ modify=Промени
[form]
UserName=Потребителско име
RepoName=Име на хранилище
RepoName=Име на хранилището
Email=Адрес на ел. поща
Password=Парола
Retype=Повторно паролата
SSHTitle=Име на SSH ключ
HttpsUrl=HTTPS URL
HttpsUrl=HTTPS URL адрес
PayloadUrl=URL адрес на изпращане
TeamName=Име на екипа
AuthName=Име за удостоверение
AuthName=Име на удостоверението
AdminEmail=Ел. поща на администратора
require_error=` не може да бъде празен.`
@@ -181,8 +192,8 @@ min_size_error=` трябва да съдържа поне %s знака.`
max_size_error=` трябва да съдържа най-много %s знака.`
email_error=` не е валиден адрес на ел. поща.`
url_error=` не е валиден URL адрес.`
unknown_error=Непозната грешка:
captcha_incorrect=Captcha не съвпада.
unknown_error=Неизвестна грешка:
captcha_incorrect=Captcha не е потвърдена.
password_not_match=Паролата и потвърждението ѝ не съвпадат.
username_been_taken=Потребителското име вече се ползва.
@@ -190,20 +201,20 @@ repo_name_been_taken=Името на хранилището вече се пол
org_name_been_taken=Името на организацията вече се ползва.
team_name_been_taken=Името на екипа вече се ползва.
email_been_used=Този адрес на ел. поща вече се ползва.
illegal_team_name=Името на екип съдържа недопустими знаци.
username_password_incorrect=Потребителското име или паролата не е вярна.
enterred_invalid_repo_name=Моля уверете се, че името на хранилището е въведено правилно.
illegal_team_name=Името на екипа съдържа недопустими знаци.
username_password_incorrect=Потребителското име или паролата не са верни.
enterred_invalid_repo_name=Моля уверете се, че въведеното име на хранилище е вярно.
enterred_invalid_owner_name=Моля уверете се, че въведеното име на притежател е вярно.
enterred_invalid_password=Моля уверете се, че въведената парола е вярна.
user_not_exist=Даденият потребител не съществува.
last_org_owner=Премахване на последния потребител от екип притежатели не е позволено, тъй като винаги трябва да има поне един притежател в дадена организация.
invalid_ssh_key=За съжаление, ние не сме в състояние да удостоверим вашия SSH ключ: %s
unable_verify_ssh_key=Gogs не може да провери вашия SSH ключ, но предполагаме, че е валиден. Моля, проверете го.
invalid_ssh_key=За съжаление, ние не сме в състояние да проверим Вашия SSH ключ: %s
unable_verify_ssh_key=Gogs не може да провери Вашия SSH ключ, но предполагаме, че е валиден. Моля, проверете го.
auth_failed=Неуспешно удостоверяване: %v
still_own_repo=Вашият профил притежава поне едно хранилище. Първо трябва да ги изтриете или да ги прехвърлите на друг потребител.
still_has_org=Вашият профил все още е член на поне една организация. Първо трябва да напуснете или изтриете вашите членства.
still_has_org=Вашият профил все още е член на поне една организация. Първо трябва да напуснете или изтриете Вашите членства.
org_still_own_repo=Тази организация все още притежава хранилище. Първо трябва да го изтриете или да го прехвърлите на друга организация.
still_own_user=Това удостоверяване се използва от поне един потребител. Моля премахнете потребителите към него и опитайте отново.
@@ -211,8 +222,8 @@ still_own_user=Това удостоверяване се използва от
target_branch_not_exist=Целевият клон не съществува.
[user]
change_avatar=Сменете вашия аватар на gravatar.com
change_custom_avatar=Промяна на вашия аватар в настройките
change_avatar=Сменете Вашия аватар на gravatar.com
change_custom_avatar=Сменете Вашия аватар в настройките
join_on=Регистриран на
repositories=Хранилища
activity=Публична дейност
@@ -234,20 +245,20 @@ delete=Изтрий профил
uid=UID
public_profile=Публичен профил
profile_desc=Вашият адрес на ел. поща е публичен и ще бъде използван за всички свързани с профила ви уведомления и всички уеб базирани операции, направени чрез сайта.
profile_desc=Вашият адрес на ел. поща е публичен и ще бъде използван за всички свързани с профила Ви уведомления и всички уеб базирани операции, направени чрез сайта.
full_name=Пълно име
website=Уебсайт
location=Местоположение
update_profile=Актуализиране на профила
update_profile_success=Вашият профил е актуализиран успешно.
location=Локация
update_profile=Обнови профила
update_profile_success=Вашият профил е запазен успешно.
change_username=Потребителското име е променено
change_username_desc=Променяте потребителско Ви име. Това ще засегне всички връзки сочещи към профила Ви. Желаете ли да продължите?
change_username_prompt=Този промяна ще засегне всички връзки сочещи към профила Ви.
continue=Продължи
cancel=Отказ
enable_custom_avatar=Разреши потребителски аватар
enable_custom_avatar_helper=Включете тази опция, за да забраните зареждане от Gravatar
choose_new_avatar=Изберете нов аватар
choose_new_avatar=Избери нов аватар
update_avatar=Обнови настройките на аватара
uploaded_avatar_not_a_image=Каченият файл не е изображение.
no_custom_avatar_available=Невъзможно използване на външен аватар, защото не е активирано.
@@ -256,23 +267,27 @@ update_avatar_success=Настройките на аватара са запаз
change_password=Промени парола
old_password=Текуща парола
new_password=Нова парола
retype_new_password=Повторно новата парола
password_incorrect=Въведената парола не е вярна.
change_password_success=Вашата парола е променена успешно. Вече може да влизате, използвайки тази нова парола.
emails=Адреси на ел. поща
manage_emails=Управление на адреси на ел. поща
email_desc=Вашият основен адрес на ел. поща ще се използва за известия и други операции.
email_desc=Вашият основен адрес на ел. поща ще се използва за изпращане на уведомления и други операции.
primary=Основен
primary_email=Задаване като основен
primary_email=Задай като основен
delete_email=Изтрий
email_deletion=Изтрий ел. поща
email_deletion_desc=При изтриване на тази ел. поща ще се премахне свързаната информация от Вашия профил. Желаете ли да продължите?
email_deletion_success=Ел. пощата беше изтрита успешно!
add_new_email=Добавяне на нов адрес на ел. поща
add_email=Добави ел. поща
add_email_confirmation_sent=Ново писмо за потвърждение е изпратено до <b>%s</b>. Моля проверете пощенската си кутия в рамките на следващите %d часа, за да завършите процеса на регистрация.
add_email_success=Нов адрес на ел. поща беше добавен успешно.
add_email_confirmation_sent=Ново писмо за потвърждение е изпратено до '%s'. Моля проверете пощенската си кутия в рамките на следващите %d часа, за да завършите процеса на регистрация.
add_email_success=Ваш нов адрес на ел. поща е добавен успешно.
manage_ssh_keys=Управление на SSH ключове
add_key=Добави ключ
ssh_desc=Това е списък на SSH ключове, свързани с вашия акаунт. Тъй като тези ключове позволяват на всеки, който ги използва да получи достъп до хранилищата ви, много е важно да се уверите, че ги разпознавате.
ssh_desc=Това е списък на SSH ключове, свързани с Вашия акаунт. Тъй като тези ключове позволяват на всеки, който ги използва да получи достъп до хранилищата Ви, много е важно да се уверите, че ги разпознавате.
ssh_helper=<strong>Не знам как?</strong> Проверете на GitHub упътването как да <a href="%s">създадете свои собствени SSH ключове</a> или решаване на <a href="%s">Общи проблеми</a>, които може да възникнат при използване на SSH.
add_new_key=Добавяне на SSH ключ
ssh_key_been_used=Съдържанието на публичния ключ е използвано.
@@ -281,8 +296,8 @@ key_name=Име на ключа
key_content=Съдържание
add_key_success=Новият SSH ключ '%s' е добавен успешно!
delete_key=Изтрий
ssh_key_deletion=Изтриване на SSH ключ
ssh_key_deletion_desc=Изтривайки този SSH ключ премахвате всякакъв достъп за Вашия профил. Желаете ли да продължите?
ssh_key_deletion=Изтрий SSH ключ
ssh_key_deletion_desc=При изтриване на този SSH ключ ще се премахнат свързаните права за достъп за Вашия профил. Желаете ли да продължите?
ssh_key_deletion_success=SSH ключа беше изтрит успешно!
add_on=Добавен на
last_used=Последно използван на
@@ -298,31 +313,31 @@ unbind_success=Социалния профил е освободен.
manage_access_token=Управление на индивидуални токени за достъп
generate_new_token=Генериране на нов токен
tokens_desc=Генерирани токени, които могат да се използват за достъп до API-то на Gogs.
new_token_desc=Всеки токен ще има пълен достъп до вашия профил.
token_name=Име на токен
new_token_desc=Всеки токен ще има пълен достъп до Вашия профил.
token_name=Име на токена
generate_token=Генериране на токен
generate_token_succees=Успешно е генериран токен за достъп. Уверете се, че сте го копирали, тъй като няма да можете да го видите отново!
delete_token=Изтрий
access_token_deletion=Изтриване на индивидуален токен за достъп
access_token_deletion_desc=Изтриването на този индивидуален токен за достъп ще премахне всички свързани права на приложението. Желаете ли да продължите?
access_token_deletion=Изтрий индивидуален токен за достъп
access_token_deletion_desc=При изтриване на този индивидуален токен за достъп ще се премахнат всички свързани права на приложението. Желаете ли да продължите?
delete_token_success=Индивидуалния токен за достъп е изтрит успешно! Не забравяйте да преконфигурирате приложението също.
delete_account=Изтриване на вашия профил
delete_prompt=Тази операция ще изтрие Вашия профил завинаги и <strong>НЕ МОЖЕ</strong> да се отмени!
confirm_delete_account=Потвърждаване на изтриването
delete_account=Изтрий собствен профил
delete_prompt=Тази операция ще изтрие Вашия профил завинаги и тя <strong>НЕ МОЖЕ</strong> да бъде отменена в последствие!
confirm_delete_account=Потвърди изтриването
delete_account_title=Изтрий профил
delete_account_desc=Този профил ще бъде окончателно изтрит. Желаете ли да продължите?
[repo]
owner=Притежател
repo_name=Име на хранилище
repo_name=Име на хранилището
repo_name_helper=Добро име на хранилище е име, състоящо от кратки, запомнящи се и уникални ключови думи.
visibility=Видимост
visiblity_helper=Това хранилище е <span class="ui red text">Частно</span>
visiblity_fork_helper=(Промяна на тази стойност ще се отрази на всички разклонения)
fork_repo=Разклони хранилището
fork_from=Разклонение от
fork_visiblity_helper=Не можете да промените видимостта на разклонено хранилище.
fork_visiblity_helper=Не може да променяте видимостта на разклонено хранилище.
repo_desc=Описание
repo_lang=Език
repo_lang_helper=Изберете .gitignore файлове
@@ -335,8 +350,8 @@ create_repo=Създай хранилище
default_branch=Клон по подразбиране
mirror_interval=Интервал на отразяване (часове)
form.name_reserved=Името на хранилище '%s' е запазено.
form.name_pattern_not_allowed=Име на хранилище от вида '%s' не е позволено.
form.name_reserved=Името на хранилището '%s' е запазено.
form.name_pattern_not_allowed=Име на хранилището от вида '%s' не е позволено.
need_auth=Изисква удостоверяване
migrate_type=Тип мигриране
@@ -377,7 +392,7 @@ commits=Ревизии
releases=Издания
file_raw=Суров
file_history=История
file_view_raw=Суров вид
file_view_raw=Виж суров
file_permalink=Постоянна връзка
commits.commits=Ревизии
@@ -403,14 +418,14 @@ issues.new.clear_assignee=Изчисти изпълнител
issues.new.no_assignee=Няма изпълнител
issues.create=Докладвай проблем
issues.new_label=Нов етикет
issues.new_label_placeholder=Име на етикет...
issues.new_label_placeholder=Име на етикета...
issues.create_label=Създай етикет
issues.open_tab=%d отворени
issues.close_tab=%d затворени
issues.filter_label=Етикет
issues.filter_label_no_select=Не е избран етикет
issues.filter_milestone=Етап
issues.filter_milestone_no_select=Липсван избран етап
issues.filter_milestone_no_select=Липсва избран етап
issues.filter_assignee=Изпълнител
issues.filter_assginee_no_select=Няма избран изпълнител
issues.filter_type=Тип
@@ -436,11 +451,11 @@ issues.commented_at=`коментира <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=Все още няма съдържание.
issues.close_issue=Затвори
issues.close_comment_issue=Затвори и коментирай
issues.reopen_issue=Отвори отново
issues.reopen_comment_issue=Отвори отново и коментирай
issues.reopen_issue=Отвори повторно
issues.reopen_comment_issue=Отвори повторно и коментирай
issues.create_comment=Коментирай
issues.closed_at=`затвори <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`отново отвори <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`повторно отвори <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`посочи този проблем от ревизия <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.poster=Участник
issues.admin=Администратор
@@ -450,15 +465,15 @@ issues.sign_in_require_desc=за да се включите в този разг
issues.edit=Редакция
issues.cancel=Отказ
issues.save=Запис
issues.label_title=Име на етикет
issues.label_title=Име на етикета
issues.label_color=Цвят на етикет
issues.label_count=%d етикети
issues.label_open_issues=%d отворени проблема
issues.label_edit=Редакция
issues.label_delete=Изтрий
issues.label_modify=Промяна на етикет
issues.label_deletion=Премахване на етикет
issues.label_deletion_desc=Изтриването на този етикет ще премахне информацията за него във всички свързани проблеми. Желаете ли да продължите?
issues.label_deletion=Изтрий етикет
issues.label_deletion_desc=При изтриване на този етикет ще се премахне информацията за него във всички свързани проблеми. Желаете ли да продължите?
issues.label_deletion_success=Етикетът е изтрит успешно!
pulls.compare_changes=Сравни промените
@@ -475,7 +490,7 @@ pulls.merged_title_desc=обедини %[1]d ревизии от <code>%[2]s</co
pulls.tab_conversation=Разговор
pulls.tab_commits=Ревизии
pulls.tab_files=Променени файлове
pulls.reopen_to_merge=Моля отново заявете тази заявка за сливане за да се извърши обединяване.
pulls.reopen_to_merge=Моля повторно отворете тази заявка за сливане за да се извърши обединяване.
pulls.merged=Обединени
pulls.has_merged=Тази заявка за сливане е обединена успешно!
pulls.data_broken=Данните от тази заявка за сливане са невалидни поради изтрита информация за някое разклонение.
@@ -504,7 +519,7 @@ milestones.edit_subheader=Въведете точни описания на ет
milestones.cancel=Отказ
milestones.modify=Промяна на етап
milestones.edit_success=Промените в етап '%s' са запазени успешно!
milestones.deletion=Премахване на етап
milestones.deletion=Изтрий етап
milestones.deletion_desc=При изтриване на етап ще се премахне информацията за него от всички свързани проблеми. Желаете ли да продължите?
milestones.deletion_success=Етапът е изтрит успешно!
@@ -516,10 +531,10 @@ settings.githooks=Git куки
settings.basic_settings=Основни настройки
settings.danger_zone=Опасната зона
settings.site=Официален сайт
settings.update_settings=Промени настройките
settings.update_settings=Обнови настройките
settings.change_reponame_prompt=Тази промяна ще засегне връзките, които се отнасят до това хранилището.
settings.transfer=Прехвърляне на притежание
settings.transfer_desc=Прехвърля това хранилище на друг потребител или на организация, в която имате права на администратор.
settings.transfer=Прехвърли притежание
settings.transfer_desc=Прехвърля това хранилище на друг потребител или към организация, в която имате права на администратор.
settings.new_owner_has_same_repo=Новият притежател вече има хранилище със същото име. Изберете друго име.
settings.delete=Изтриване на това хранилище
settings.delete_desc=След като изтриете хранилището, няма връщане назад. Моля, бъдете сигурни.
@@ -531,11 +546,11 @@ settings.delete_notices_2=- Тази операция ще изтрие всич
settings.delete_notices_fork_1=- Ако това хранилище е публично, всички негови разклонения ще останат независими след изтриването му.
settings.delete_notices_fork_2=- Ако това хранилище е частно, всички негови разклонения ще бъдат премахнати по време на изтриването.
settings.delete_notices_fork_3=- Ако желаете да запазите всички разклонения след изтриването му, първо направете хранилището публично.
settings.update_settings_success=Настройките на хранилището са актуализирани успешно.
settings.update_settings_success=Настройките на хранилището са запазени успешно.
settings.transfer_owner=Нов притежател
settings.make_transfer=Прехвърляне
settings.make_transfer=Прехвърли
settings.transfer_succeed=Притежанието на хранилището е прехвърлено успешно.
settings.confirm_delete=Потвърждаване на изтриването
settings.confirm_delete=Потвърди изтриването
settings.add_collaborator=Добави нов сътрудник
settings.add_collaborator_success=Добавен е нов сътрудник.
settings.remove_collaborator_success=Сътрудникът е премахнат.
@@ -543,7 +558,7 @@ settings.user_is_org_member=Потребителят е член на орган
settings.add_webhook=Добави уеб-кука
settings.hooks_desc=Уеб-куките много приличат на обикновен HTTP POST тригер. Когато нещо се случи в Gogs, ние ще изпратим уведомление до сървъра, който посочите. Научете повече в <a target="_blank" href="%s">Ръководство за уеб-куки</a>.
settings.webhook_deletion=Изтрий уеб-кука
settings.webhook_deletion_desc=Изтриването на тази уеб-кука ще премахне информацията за нея и цялата хронология на нейното изпращане. Желаете ли да продължите?
settings.webhook_deletion_desc=При изтриване на тази уеб-кука ще се премахне информацията за нея и цялата хронология на нейното изпращане. Желаете ли да продължите?
settings.webhook_deletion_success=Уеб-куката е изтрита успешно!
settings.webhook.request=Заявка
settings.webhook.response=Отговор
@@ -551,10 +566,10 @@ settings.webhook.headers=Заглавки
settings.webhook.payload=Съдържание
settings.webhook.body=Тяло
settings.githooks_desc=Git куките се изпълняват от Git. Вие може да промените файловете с поддържаните куки в списъка по-долу, за да изпълните външни операции.
settings.githook_edit_desc=Ако куката е неактивна, ще бъдат представено примерно съдържание. Ако оставите съдържанието празно, то тази кука ще бъде изключена.
settings.githook_name=Име на кука
settings.githook_content=Съдържание на кука
settings.update_githook=Обнови кука
settings.githook_edit_desc=Ако куката е неактивна, ще бъде представено примерно съдържание. Ако оставите съдържанието празно, то тази кука ще бъде изключена.
settings.githook_name=Име на куката
settings.githook_content=Съдържание на куката
settings.update_githook=Обнови куката
settings.add_webhook_desc=Gogs ще изпрати <code>POST</code> заявка към указания URL адрес заедно с информация за събитието, което е настъпило. Също можете да укажете в какъв формат желаете да получите данните при задействане на куката (JSON, x-www-form-urlencoded, XML) и др. Допълнително описание можете да намерите в нашето <a target="_blank" href="%s">Ръководство за уеб-куки</a>.
settings.payload_url=URL адрес на изпращане
settings.content_type=Тип на съдържанието
@@ -573,8 +588,8 @@ settings.event_push_desc=Git предаване към хранилището
settings.active=Активна
settings.active_helper=Подробности относно събитието, което е задействало куката, също ще бъдат изпратени.
settings.add_hook_success=Новата уеб-кука е добавена успешно.
settings.update_webhook=Промени уеб-куката
settings.update_hook_success=Уеб-куката е променена успешно.
settings.update_webhook=Обнови уеб-куката
settings.update_hook_success=Уеб-куката е запазена успешно.
settings.delete_webhook=Изтрий уеб-куката
settings.recent_deliveries=Последни изпращания
settings.hook_type=Тип на куката
@@ -591,7 +606,7 @@ settings.key_been_used=Съдържанието на ключа за внедр
settings.key_name_used=Ключ за внедряване с такова име вече съществува.
settings.add_key_success=Новият ключ за внедряване '%s' е добавен успешно!
settings.deploy_key_deletion=Изтрий ключ за внедряване
settings.deploy_key_deletion_desc=Изтриването на този ключ за внедряване ще премахне свързаните права за достъп до това хранилище. Желаете ли да продължите?
settings.deploy_key_deletion_desc=При изтриването на този ключ за внедряване ще се премахнат свързаните права за достъп до това хранилище. Желаете ли да продължите?
settings.deploy_key_deletion_success=Ключът за внедряване е изтрит успешно!
diff.browse_source=Преглед на кода
@@ -611,7 +626,7 @@ release.stable=Стабилни
release.edit=редактиране
release.ahead=<strong>%d</strong> ревизии на %s след това издание
release.source_code=Изходен код
release.tag_name=Име на маркер
release.tag_name=Име на маркера
release.target=Цел
release.tag_helper=Изберете съществуващ маркер или създайте нов маркер по време на публикуване.
release.release_title=Заглавие на изданието
@@ -630,9 +645,8 @@ release.tag_name_already_exist=Издание с това име на марке
[org]
org_name_holder=Име на организацията
org_name_helper=Добрите имена на организация са кратки и запомнящи се.
org_email_helper=Ел. пощата на организацията получава всички уведомления и потвърждения.
create_org=Създай организация
repo_updated=Актуализиране
repo_updated=Обновено
people=Хора
invite_someone=Поканете някого
teams=Екипи
@@ -646,23 +660,23 @@ team_name_helper=Ще използвате това име при спомена
team_desc_helper=Каква е целта на този екип?
team_permission_desc=Какво ниво на достъп трябва да има този екип?
form.name_reserved=Името на организация '%s' е запазено.
form.name_pattern_not_allowed=Име на организация от вида '%s' не е разрешено.
form.name_reserved=Името на организацията '%s' е запазено.
form.name_pattern_not_allowed=Име на организацията от вида '%s' не е разрешено.
settings=Настройки
settings.options=Опции
settings.full_name=Пълно име
settings.website=Уебсайт
settings.location=Местоположение
settings.update_settings=Актуализирай настройките
settings.change_orgname=Името на екипа е променено
settings.change_orgname_desc=Името на организацията ще бъде променено. Това ще засегне всички връзки свързани с организацията. Желаете ли да продължите?
settings.location=Локация
settings.update_settings=Обнови настройките
settings.update_setting_success=Настройките на организацията са запазени успешно.
settings.change_orgname_prompt=Този промяна ще засегне всички връзки сочещи към организацията.
settings.update_avatar_success=Настройките на аватара на организацията са запазени успешно.
settings.delete=Изтрий организацията
settings.delete_account=Изтриване на тази организация
settings.delete_prompt=Организацията ще бъде изтрита и <strong>НЕ МОЖЕ</strong> да се върне!
settings.delete_prompt=Организацията ще бъде изтрита и операцията <strong>НЕ МОЖЕ</strong> да бъде отменена в последствие!
settings.confirm_delete_account=Потвърди изтриването
settings.delete_org_title=Изтриване на организацията
settings.delete_org_title=Изтрий организацията
settings.delete_org_desc=Тази организация ще бъде окончателно изтрита. Желаете ли да продължите?
settings.hooks_desc=Добави уеб-куки, които ще бъдат използвани от <strong>всички хранилища</strong> в тази организация.
@@ -690,14 +704,14 @@ teams.no_desc=Този екип няма описание
teams.settings=Настройки
teams.owners_permission_desc=Притежателите имат пълен достъп до <strong>всички хранилища</strong> и имат <strong>права на администратори</strong> на организацията.
teams.members=Членовете на екипа
teams.update_settings=Актуализирай настройките
teams.update_settings=Обнови настройките
teams.delete_team=Изтриване на този екип
teams.add_team_member=Добавяне на член в екипа
teams.add_team_member=Добави член на екипа
teams.delete_team_title=Изтрий екипа
teams.delete_team_desc=Тъй като този екип ще бъдат изтрит, членовете на този екип може да загубят достъп до някои хранилища. Желаете ли да продължите?
teams.delete_team_desc=Тъй като този екип ще бъдат изтрит, членовете му може да загубят достъп до някои хранилища. Желаете ли да продължите?
teams.delete_team_success=Този екип е бил изтрит успешно.
teams.read_permission_desc=Този екип предоставя достъп за <strong>четене</strong>: членове могат да разглеждат и клонират хранилищата на екипа.
teams.write_permission_desc=Този екип предоставя права за <strong>писане</strong>: членовете могат да четат от и предават към хранилищата на екипа.
teams.write_permission_desc=Този екип предоставя достъп за <strong>писане</strong>: членовете могат да четат от и предават към хранилищата на екипа.
teams.admin_permission_desc=Този екип предоставя <strong>администраторски</strong> достъп: членовете могат да четат от, да предават към и да добавя нови сътрудници към хранилищата на екипа.
teams.repositories=Хранилища на екипа
teams.add_team_repository=Добави хранилище на екипа
@@ -707,14 +721,15 @@ teams.add_nonexistent_repo=Хранилището, което се опитва
[admin]
dashboard=Табло
users=Потребители
organizations=Организация
organizations=Организации
repositories=Хранилища
authentication=Удостоверявания
config=Конфигурация
notices=Системни известия
monitor=Наблюдение
prev=Предишен
next=Следващ
first_page=Първа
last_page=Последна
total=Общо: %d
dashboard.statistic=Статистика
dashboard.operations=Операции
@@ -725,9 +740,9 @@ dashboard.operation_switch=Превключи
dashboard.operation_run=Изпълни
dashboard.clean_unbind_oauth=Почисти несвързани OAuthes
dashboard.clean_unbind_oauth_success=Всички несвързани OAuthes са изтрити успешно.
dashboard.delete_inactivate_accounts=Изтриване на всички неактивни профили
dashboard.delete_inactivate_accounts=Изтрий всички неактивни профили
dashboard.delete_inactivate_accounts_success=Всички неактивни профили са изтрити успешно.
dashboard.delete_repo_archives=Изтриване на всички архиви на хранилища
dashboard.delete_repo_archives=Изтрий всички архиви на хранилища
dashboard.delete_repo_archives_success=Всички архиви на хранилищата са изтрити успешно.
dashboard.git_gc_repos=Почисти изтрити данни в хранилищата
dashboard.git_gc_repos_success=Всички хранилища са почистени от изтрити данни успешно.
@@ -741,9 +756,9 @@ dashboard.current_goroutine=Текущи Goroutines
dashboard.current_memory_usage=Текущо използвана памет
dashboard.total_memory_allocated=Общо заделена памет
dashboard.memory_obtained=Получена памет
dashboard.pointer_lookup_times=Време за обхождане на указатели
dashboard.memory_allocate_times=Време за заделяне на памет
dashboard.memory_free_times=Време за освобождаване на памет
dashboard.pointer_lookup_times=Брой обхождания на указатели
dashboard.memory_allocate_times=Брой заделяния на памет
dashboard.memory_free_times=Брой освобождавания на памет
dashboard.current_heap_usage=Текущо използвана осн. памет
dashboard.heap_memory_obtained=Получена осн. памет
dashboard.heap_memory_idle=Празна осн. памет
@@ -759,40 +774,44 @@ dashboard.mcache_structures_obtained=Получени MCache обекти
dashboard.profiling_bucket_hash_table_obtained=Получени Profiling Bucket Hash Table
dashboard.gc_metadata_obtained=Получени GC метаданни
dashboard.other_system_allocation_obtained=Получена друга системна памет
dashboard.next_gc_recycle=Слeдващо рециклиране на GC
dashboard.next_gc_recycle=Следващо рециклиране на GC
dashboard.last_gc_time=Време от последен GC
dashboard.total_gc_time=Общо време за GC
dashboard.total_gc_pause=Общо пауза за GC
dashboard.last_gc_pause=Последна пауза за GC
dashboard.gc_times=Брой GC
users.user_manage_panel=Панел за управление на потребителите
users.new_account=Създаване на нов профил
users.user_manage_panel=Управление на потребителя
users.new_account=Създай нов профил
users.name=Име
users.activated=Активиран
users.admin=Администратор
users.repos=Хранилища
users.created=Създаване
users.send_register_notify=Прати уведомление на потребителя при регистрация
users.new_success=Новият профил '%s' е добавен успешно.
users.edit=Редакция
users.auth_source=Източник за удостоверяване
users.local=Локално
users.auth_login_name=Потребителско име за удостоверяване
users.update_profile_success=Профилът е обновен успешно.
users.edit_account=Редактиране на профил
users.password_helper=Оставете празна ако не се променя.
users.update_profile_success=Профилът е запазен успешно.
users.edit_account=Редактирай профил
users.is_activated=Този профил е активиран
users.is_admin=Този профил има административни права
users.allow_git_hook=Този профил има разрешение да създава Git куки
users.update_profile=Обнови профила
users.delete_account=Изтриване на този профил
users.still_own_repo=Този профил притежава поне едно хранилище. Първо трябва да го изтриете или да го прехвърлите на друг потребител.
users.delete_account=Изтрий този профил
users.still_own_repo=Този профил притежава поне едно хранилище. Първо трябва да изтриете хранилището или да го прехвърлите на друг потребител.
users.still_has_org=Този профил е член на поне една организация. Първо трябва да напуснете или изтриете тези организации.
users.deletion_success=Профилът е изтрит успешно!
orgs.org_manage_panel=Управление на организациите
orgs.org_manage_panel=Управление на организацията
orgs.name=Име
orgs.teams=Екипи
orgs.members=Членове
repos.repo_manage_panel=Управление на хранилищата
repos.repo_manage_panel=Управление на хранилището
repos.owner=Притежател
repos.name=Име
repos.private=Лично
@@ -800,41 +819,47 @@ repos.watches=Наблюдавания
repos.stars=Харесвания
repos.issues=Проблеми
auths.auth_manage_panel=Управление на удостоверяването
auths.auth_manage_panel=Управление на удостоверявания
auths.new=Добави нов източник за удостоверяване
auths.name=Име
auths.type=Тип
auths.enabled=Активен
auths.updated=Актуализиран
auths.updated=Обновен
auths.auth_type=Тип на удостоверяване
auths.auth_name=Име на удостоверяване
auths.domain=Домейн
auths.host=Сървър
auths.port=Порт
auths.bind_dn=Домейн за bind
auths.bind_password=Парола за bind
auths.bind_dn=Име (DN) за свръзка
auths.bind_password=Парола за свързка
auths.bind_password_helper=Внимание: Тази парола се запазва некриптирана. Моля използвайте потребител, който няма административен достъп.
auths.user_base=База с потребители
auths.user_dn=Име (DN) на потребител
auths.attribute_name=Атрибут за име
auths.attribute_surname=Атрибут за фамилия
auths.attribute_mail=Атрибут за ел. поща
auths.filter=Филтър за потребители
auths.admin_filter=Филтър за администратори
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Тип на SMTP удостоверяване
auths.smtp_auth=SMTP удостоверяване
auths.smtphost=SMTP сървър
auths.smtpport=SMTP порт
auths.allowed_domains=Разрешени домейни
auths.allowed_domains_helper=Оставете празно за да не се ограничават домейните. За множество домейни използвайте запетая за разделител.
auths.enable_tls=Включи TLS криптиране
auths.skip_tls_verify=Пропусни проверка на TLS
auths.pam_service_name=Име на PAM услуга
auths.enable_auto_register=Включи автоматична регистрация
auths.tips=Съвети
auths.edit=Редактиране на настройки за удостоверяване
auths.edit=Редактирай настройки за удостоверяване
auths.activated=Това удостоверяване е активно
auths.update_success=Настройките за удостоверяване са обновени успешно.
auths.new_success=Новото удостоверяване '%s' е добавено успешно.
auths.update_success=Настройките за удостоверяване са запазени успешно.
auths.update=Обнови настройки за удостоверяване
auths.delete=Изтриване на това удостоверяване
auths.delete_auth_title=Изтрий удостоверяването
auths.delete_auth_desc=Това удостоверяване ще бъде изтрито. Желаете ли да продължите?
auths.deletion_success=Удостоверяването е изтрито успешно!
config.server_config=Сървърни настройки
config.app_name=Име на приложението
@@ -858,16 +883,18 @@ config.db_user=Потребител
config.db_ssl_mode=SSL режим
config.db_ssl_mode_helper=(само за postgres)
config.db_path=Път
config.db_path_helper=(само за sqlite3)
config.db_path_helper=(за "sqlite3" и "tidb")
config.service_config=Настройка на услугата
config.register_email_confirm=Изисквай потвърждение на адреси на ел. поща
config.disable_register=Изключи нови регистриции
config.disable_register=Изключи нови регистрации
config.show_registration_button=Покажи бутон за регистрация
config.require_sign_in_view=Изисквай вписване за преглед
config.mail_notify=Уведомяване по ел. поща
config.enable_cache_avatar=Включи кеширане на аватари
config.mail_notify=Уведомяване по ел. поща
config.disable_key_size_check=Изключи проверка минимален размер на ключ
config.enable_captcha=Включи Captcha
config.active_code_lives=Кодове за активиране
config.reset_password_code_lives=Кодове за ресет на парола
config.reset_password_code_lives=Кодове за изчистване на парола
config.webhook_config=Конфигурация на уеб-куки
config.queue_length=Дължина на опашка
config.deliver_timeout=Време за отказ при изпращане
@@ -887,15 +914,15 @@ config.cache_conn=Кеш на връзката
config.session_config=Конфигурация на сесии
config.session_provider=Доставчик на сесии
config.provider_config=Конфигурация на доставчик
config.cookie_name=Име на бисквитка
config.cookie_name=Име на бисквитката
config.enable_set_cookie=Включи използване на бисквитки
config.gc_interval_time=GC през интервал
config.session_life_time=Време на живот на сесиите
config.session_life_time=Период на валидност на сесиите
config.https_only=HTTPS само
config.cookie_life_time=Време на живот на бисквитките
config.cookie_life_time=Период на валидност на бисквитките
config.picture_config=Конфигурация на изображения
config.picture_service=Услуги за снимки
config.disable_gravatar=Изключване на Gravatar
config.disable_gravatar=Изключи Gravatar
config.log_config=Конфигурация на журнал
config.log_mode=Режим на журнал
@@ -904,7 +931,7 @@ monitor.name=Име
monitor.schedule=График
monitor.next=Следващ път
monitor.previous=Предишен път
monitor.execute_times=Време на изпълнение
monitor.execute_times=Брой изпълнения
monitor.process=Изпълнявани процеси
monitor.desc=Описание
monitor.start=Начален час
@@ -954,5 +981,5 @@ raw_minutes=минути
default_message=Пуснете файлове тук или щракнете за качване.
invalid_input_type=Невъзможно качване на файловете от този тип.
file_too_big=Размер на файла ({{filesize}} MB) надвишава максималния размер ({{maxFilesize}} MB).
remove_file=Премахване на файл
remove_file=Премахни файл

View File

@@ -5,7 +5,6 @@ dashboard=Übersicht
explore=Erkunden
help=Hilfe
sign_in=Anmelden
social_sign_in=Anmeldung über soziales Konto: zweiter Schritt <small>Konto verknüpfen</small>
sign_out=Abmelden
sign_up=Registrieren
register=Registrieren
@@ -54,7 +53,8 @@ code=Code
[install]
install=Installation
title=Installation für erstmaligen Start
requite_db_desc=Gogs erfordert MySQL, PostgreSQL oder SQLite 3, aber SQLite3 ist in der offiziellen binären Version akiviert.
docker_helper=Wenn Gogs innerhalb Docker läuft, lies dir bitte die <a target="_blank" href="%s">Guidelines</a> genau durch, bevor du irgendwas auf dieser Seite änderst!
requite_db_desc=Gogs benötigt MySQL, PostgreSQL, SQLite3 oder TiDB.
db_title=Datenbankeinstellungen
db_type=Datenbanktyp
host=Host
@@ -64,8 +64,11 @@ db_name=Datenbankname
db_helper=Bitte verwenden InnoDB-Engine mit utf8_general_ci Zeichensatz für MySQL.
ssl_mode=SSL-Modus
path=Pfad
sqlite_helper=Der Dateipfad des SQLite3 Datenbank.
err_empty_sqlite_path=Pfad zur SQLite3-Datenbank darf nicht leer sein.
sqlite_helper=Der Dateipfad für zur SQLite3 oder TiDB Datenbank.
err_empty_db_path=SQLite3 oder TiDB Datenbankpfad darf nicht leer sein.
err_invalid_tidb_name=Der TiDB Datenbankname darf kein "." und kein "-" enthalten.
no_admin_and_disable_registration=Du kannst die Registrierung nicht deaktivieren, ohne ein Administratorkonto zu erstellen.
err_empty_admin_password=Administrator-Passwort darf nicht leer sein.
general_title=Allgemeine Einstellungen von Gogs
app_name=Anwendungsname
@@ -99,6 +102,8 @@ disable_gravatar=Gravatar-Dienst deaktivieren
disable_gravatar_popup=Gravatar und benutzerdefinierte Quellen deaktivieren, alle Avatare werden standardmäßig vom Nutzer hochgeladen oder sind Standardavatare.
disable_registration=Benutzerregistrierung deaktivieren
disable_registration_popup=Deaktiviere die Benutzerregistrierung, nur Administratoren können Benutzerkonten anlegen.
enable_captcha=Captcha aktivieren
enable_captcha_popup=Benötigt Captcha-Überprüfung für Registrierung durch Benutzer.
require_sign_in_view=Erfordere Anmeldung, um Inhalte anzusehen
require_sign_in_view_popup=Lediglich angemeldete Benutzer können Inhalte betrachten, Gäste sehen nur die Anmelden/Registrieren Seite.
admin_setting_desc=Sie müssen jetzt noch keinen Administrator-Account anlegen. Der erste Benutzer ("ID=1") erhält automatisch Administrationsrechte.
@@ -143,7 +148,7 @@ forgot_password=Passwort vergessen
forget_password=Passwort vergessen?
sign_up_now=Du willst ein Konto? Jetzt registrieren!
confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Kontrolliere dein Postfach innerhalb der nächsten %d Stunden, um die Registrierung abzuschließen.
sign_in_email=Melde dich mit deiner E-Mail-Adresse an
sign_in_to_account=Mit deinem Konto anmelden
active_your_account=Aktiviere dein Konto
resent_limit_prompt=Es tut uns leid, du sendest zu häufig Aktivierungs-E-Mails. Bitte warte 3 Minuten.
has_unconfirmed_mail=Hallo %s, du hast eine unbestätigte E-Mail-Adresse (<b>%s</b>). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue benötigtst, klicke bitte auf den folgenden Button.
@@ -155,6 +160,12 @@ invalid_code=Es tut uns leid, der Bestätigungscode ist abgelaufen oder ungülti
reset_password_helper=Hier klicken, um das Passwort zurückzusetzen
password_too_short=Das Passwort muss mindenstens 6 Zeichen lang sein
[mail]
activate_account=Bitte aktiviere dein Konto
activate_email=Verifiziere deine E-Mail-Adresse
reset_password=Setze dein Passwort zurück
register_success=Registrierung erfolgreich, Willkommen
[modal]
yes=Ja
no=Nein
@@ -241,7 +252,7 @@ location=Standort
update_profile=Profil aktualisieren
update_profile_success=Profil aktualisiert
change_username=Benutzername geändert
change_username_desc=Benutzername wurde geändert, möchtest du fortfahren? Dies beeinträchtigt sämtliche Links, die dein Konto betreffen.
change_username_prompt=Diese Änderung wird sich auf die Linkbezüge zu deinem Account auswirken.
continue=Weiter
cancel=Abbrechen
@@ -256,6 +267,7 @@ update_avatar_success=Deine Avatar-Einstellung wurde aktualisiert.
change_password=Passwort ändern
old_password=Aktuelles Passwort
new_password=Neues Passwort
retype_new_password=Neues Passwort erneut eingeben
password_incorrect=Aktuelles Passwort ist nicht korrekt.
change_password_success=Passwort geändert. Du kannst dich jetzt mit dem neuen Passwort anmelden.
@@ -265,9 +277,12 @@ email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen und andere
primary=Primär
primary_email=Als primäre Adresse verwenden
delete_email=Löschen
email_deletion=E-Mail löschen
email_deletion_desc=Das Löschen dieser E-Mail Adresse wird alle Informationen entfernen, die mit dieser E-Mail Adresse verknüpft sind. Willst du fortfahren?
email_deletion_success=E-Mail-Adresse wurde erfolgreich gelöscht!
add_new_email=Neue E-Mail-Adresse hinzufügen
add_email=E-Mail-Adresse hinzufügen
add_email_confirmation_sent=Eine neue Bestätigungsmail wurde an <b>%s</b> gesendet, bitte überprüfen Sie Ihren Posteingang innerhalb von %d Stunden um die Bestätigung abzuschließen.
add_email_confirmation_sent=Eine neue Bestätigungsmail wurde an '%s' gesendet, bitte überprüfen Sie Ihren Posteingang innerhalb von %d Stunden um die Bestätigung abzuschließen.
add_email_success=Deine neue E-Mail-Adresse wurde erfolgreich hinzugefügt.
manage_ssh_keys=SSH-Schlüssel verwalten
@@ -516,7 +531,7 @@ settings.githooks=Git-Hooks
settings.basic_settings=Grundeinstellungen
settings.danger_zone=Gefahrenzone
settings.site=Offizielle Webseite
settings.update_settings=Aktualisierungseinstellungen
settings.update_settings=Einstellungen speichern
settings.change_reponame_prompt=Diese Änderung wirkt sich darauf aus, wie sich Links auf das Repository beziehen.
settings.transfer=Besitz übertragen
settings.transfer_desc=Übertrage dieses Repository einem anderen Benutzer oder einer Organisation in der du Admin-Rechte hast.
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Ein Release mit diesem Tag existiert bereits.
[org]
org_name_holder=Name der Organisation
org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam.
org_email_helper=Die E-Mail-Adresse der Organisation erhält alle Benachrichtigungen und Bestätigungs-E-Mails.
create_org=Organisation erstellen
repo_updated=Aktualisiert
people=Personen
@@ -654,10 +668,10 @@ settings.options=Optionen
settings.full_name=Vollständiger Name
settings.website=Webseite
settings.location=Standort
settings.update_settings=Aktualisierungseinstellungen
settings.change_orgname=Organisationsname geändert
settings.change_orgname_desc=Organisationsname wurde geändert, möchtest du fortfahren? Dies beeinträchtigt sämtliche Links, die diese Organisation betreffen.
settings.update_settings=Einstellungen speichern
settings.update_setting_success=Organisationseinstellungen aktualisiert
settings.change_orgname_prompt=Diese Änderung wird sich auf die Linkbezüge zur Organisation auswirken.
settings.update_avatar_success=Avatareinstellung für die Organisation wurde erfolgreich aktualisiert.
settings.delete=Organisation löschen
settings.delete_account=Diese Organisation löschen
settings.delete_prompt=Die Organisation wird dauerhaft gelöscht. Dies kann <strong>NICHT</strong> rückgängig gemacht werden!
@@ -713,8 +727,9 @@ authentication=Authentifizierung
config=Konfiguration
notices=System-Mitteilungen
monitor=Monitoring
prev=zurück
next=vor
first_page=Erste
last_page=Letzte
total=Total: %d
dashboard.statistic=Statistik
dashboard.operations=Operationen
@@ -773,10 +788,13 @@ users.activated=Aktiviert
users.admin=Admin
users.repos=Repositorys
users.created=Erzeugt
users.send_register_notify=Send Registration Notification To User
users.new_success=Der neue Account '%s' wurde erfolgreich erstellt.
users.edit=Bearbeiten
users.auth_source=Auth-Quelle
users.auth_source=Authentifizierungsquelle
users.local=Lokal
users.auth_login_name=Auth-Login-Name
users.auth_login_name=Authentifizierung-Loginnname
users.password_helper=Leer lassen um es unverändert zu lassen.
users.update_profile_success=Kontoprofil aktualisiert
users.edit_account=Konto bearbeiten
users.is_activated=Dieses Konto ist aktiviert
@@ -786,6 +804,7 @@ users.update_profile=Kontoprofil aktualisieren
users.delete_account=Dieses Konto löschen
users.still_own_repo=Dieses Konto besitzt noch Repositories. Diese müssen zuerst gelöscht oder übertragen werden.
users.still_has_org=Dieses Konto ist noch Mitglied einer Organisation, bitte entferne diese Mitgliedschaft zuerst.
users.deletion_success=Das Konto wurde erfolgreich gelöscht!
orgs.org_manage_panel=Organisationenverwaltung
orgs.name=Name
@@ -800,8 +819,8 @@ repos.watches=Beobachtungen
repos.stars=Markierungen
repos.issues=Issues
auths.auth_manage_panel=Authentifizierung
auths.new=Neue Authentifizierungsquelle hinzufügen
auths.auth_manage_panel=Verwaltungspanel für die Authentifizierung
auths.new=Neue Quelle hinzufügen
auths.name=Name
auths.type=Typ
auths.enabled=aktiviert
@@ -813,16 +832,20 @@ auths.host=Host
auths.port=Port
auths.bind_dn=DN binden
auths.bind_password=Passwort binden
auths.bind_password_helper=Warnung: Das Passwort wird im Plaintext gespeichert. Benutze keinen Account mit hohen Zugriffsrechten.
auths.user_base=Benutzer-Such-Basis
auths.user_dn=Benutzer DN
auths.attribute_name=Vorname Attribut
auths.attribute_surname=Nachname Attribut
auths.attribute_mail=E-Mail Attribut
auths.filter=Benutzernamen Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=SMTP-Authentifizierungstyp
auths.smtp_auth=SMTP Authentifizierungstyp
auths.smtphost=SMTP-Host
auths.smtpport=SMTP-Port
auths.allowed_domains=Erlaubte Domains
auths.allowed_domains_helper=Leer lassen für keine Einschränkungen. Mehrere Domains können durch Komma "," getrennt werden.
auths.enable_tls=TLS-Verschlüsselung aktivieren
auths.skip_tls_verify=TLS-Prüfung überspringen
auths.pam_service_name=PAM Dienstname
@@ -830,11 +853,13 @@ auths.enable_auto_register=Automatische Registrierung aktivieren
auths.tips=Tipps
auths.edit=Authentifizierungseinstellungen bearbeiten
auths.activated=Diese Authentifizierung ist aktiviert
auths.update_success=Authentifizierungseinstellungen aktualisiert
auths.new_success=Neue Authentifizierung '%s' wurde erfolgreich hinzugefügt.
auths.update_success=Die Authentifizierungseinstellungen wurden erfolgreich aktualisiert.
auths.update=Authentifizierungseinstellungen aktualisieren
auths.delete=Authentifizierung löschen
auths.delete_auth_title=Authentifizierungsquelle löschen
auths.delete_auth_desc=Diese Authentifizierungsquelle wird gelöscht, möchtest du fortfahren?
auths.delete=Diese Authentifizierung löschen
auths.delete_auth_title=Löschen der Authentifizierung
auths.delete_auth_desc=Diese Authentifizierung wird gelöscht, möchtest du fortfahren?
auths.deletion_success=Authentifizierung wurde erfolgreich entfernt!
config.server_config=Server-Konfiguration
config.app_name=Anwendungsname
@@ -858,14 +883,16 @@ config.db_user=Benutzer
config.db_ssl_mode=SSL-Modus
config.db_ssl_mode_helper=(nur für "postgres")
config.db_path=Verzeichnis
config.db_path_helper=(nur für "sqlite3")
config.db_path_helper=(für "sqlite3" und "tidb")
config.service_config=Service-Einstellungen
config.register_email_confirm=E-Mail-Bestätigung bei Registrierung
config.disable_register=Registrierung deaktivieren
config.show_registration_button=Zeige die Schaltfläche Registrieren
config.require_sign_in_view=Ansehen erfordert Registrierung
config.mail_notify=E-Mail-Benachrichtigung
config.enable_cache_avatar=Avatar-Cache aktivieren
config.mail_notify=E-Mail-Benachrichtigung
config.disable_key_size_check=Prüfung der Mindestschlüssellänge deaktiveren
config.enable_captcha=Captcha aktivieren
config.active_code_lives=Aktivierungscode Lebensdauer
config.reset_password_code_lives=Passwortcode Lebensdauer
config.webhook_config=Webhook-Einstellungen

View File

@@ -5,7 +5,6 @@ dashboard = Dashboard
explore = Explore
help = Help
sign_in = Sign In
social_sign_in = Social Sign In: 2nd Step <small>associate account</small>
sign_out = Sign Out
sign_up = Sign Up
register = Register
@@ -14,7 +13,7 @@ version = Version
page = Page
template = Template
language = Language
create_new = Create new...
create_new = Create...
user_profile_and_more = User profile and more
signed_in_as = Signed in as
@@ -54,7 +53,8 @@ code = Code
[install]
install = Installation
title = Install Steps For First-time Run
requite_db_desc = Gogs requires MySQL, PostgreSQL or SQLite3.
docker_helper = If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc = Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title = Database Settings
db_type = Database Type
host = Host
@@ -64,8 +64,11 @@ db_name = Database Name
db_helper = Please use INNODB engine with utf8_general_ci charset for MySQL.
ssl_mode = SSL Mode
path = Path
sqlite_helper = The file path of SQLite3 database.
err_empty_sqlite_path = SQLite3 database path cannot be empty.
sqlite_helper = The file path of SQLite3 or TiDB database.
err_empty_db_path = SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name = TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration = You cannot disable registration without creating an admin account.
err_empty_admin_password = Admin password cannot be empty.
general_title = Application General Settings
app_name = Application Name
@@ -99,6 +102,8 @@ disable_gravatar = Disable Gravatar Service
disable_gravatar_popup = Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration = Disable Self-registration
disable_registration_popup = Disable user self-registration, only admin can create accounts.
enable_captcha = Enable Captcha
enable_captcha_popup = Require validate captcha for user self-registration.
require_sign_in_view = Enable Require Sign In to View Pages
require_sign_in_view_popup = Only signed in users can view pages, visitors will only be able to see sign in/up pages.
admin_setting_desc = You do not have to create an admin account right now, user whoever ID=1 will gain admin access automatically.
@@ -143,7 +148,7 @@ forgot_password= Forgot Password
forget_password = Forgot password?
sign_up_now = Need an account? Sign up now.
confirmation_mail_sent_prompt = A new confirmation e-mail has been sent to <b>%s</b>, please check your inbox within the next %d hours to complete the registration process.
sign_in_email = Sign in to your e-mail
sign_in_to_account = Sign in to your account
active_your_account = Activate Your Account
resent_limit_prompt = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again.
has_unconfirmed_mail = Hi %s, you have an unconfirmed e-mail address (<b>%s</b>). If you haven't received a confirmation e-mail or need to resend a new one, please click on the button below.
@@ -155,6 +160,12 @@ invalid_code = Sorry, your confirmation code has expired or not valid.
reset_password_helper = Click here to reset your password
password_too_short = Password length cannot be less then 6.
[mail]
activate_account = Please activate your account
activate_email = Verify your e-mail address
reset_password = Reset your password
register_success = Register success, Welcome
[modal]
yes = Yes
no = No
@@ -241,7 +252,7 @@ location = Location
update_profile = Update Profile
update_profile_success = Your profile has been updated successfully.
change_username = Username Changed
change_username_desc = You changed your username. This will affect the way how links relate to your account. Do you want to continue?
change_username_prompt = This change will affect the way how links relate to your account.
continue = Continue
cancel = Cancel
@@ -256,6 +267,7 @@ update_avatar_success = Your avatar setting has been updated successfully.
change_password = Change Password
old_password = Current Password
new_password = New Password
retype_new_password = Retype New Password
password_incorrect = Current password is not correct.
change_password_success = Your password was successfully changed. You can now sign using this new password.
@@ -265,9 +277,12 @@ email_desc = Your primary e-mail address will be used for notifications and othe
primary = Primary
primary_email = Set as primary
delete_email = Delete
email_deletion = E-mail Deletion
email_deletion_desc = Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success = E-mail has been deleted successfully!
add_new_email = Add new e-mail address
add_email = Add e-mail
add_email_confirmation_sent = A new confirmation e-mail has been sent to <b>%s</b>, please check your inbox within the next %d hours to complete the confirmation process.
add_email_confirmation_sent = A new confirmation e-mail has been sent to '%s', please check your inbox within the next %d hours to complete the confirmation process.
add_email_success = Your new E-mail address was successfully added.
manage_ssh_keys = Manage SSH Keys
@@ -330,7 +345,7 @@ license = License
license_helper = Select a license file
readme = Readme
readme_helper = Select a readme template
auto_init = Initialize this repository selected files and template
auto_init = Initialize this repository with selected files and template
create_repo = Create Repository
default_branch = Default Branch
mirror_interval = Mirror Interval (hour)
@@ -630,7 +645,6 @@ release.tag_name_already_exist = Release with this tag name has already existed.
[org]
org_name_holder = Organization Name
org_name_helper = Great organization names are short and memorable.
org_email_helper = Organization's E-mail receives all notifications and confirmations.
create_org = Create Organization
repo_updated = Updated
people = People
@@ -655,9 +669,9 @@ settings.full_name = Full Name
settings.website = Website
settings.location = Location
settings.update_settings = Update Settings
settings.change_orgname = Organization Name Changed
settings.change_orgname_desc = Organization name has been changed. This will affect how links relate to the organization. Do you want to continue?
settings.update_setting_success = Organization settings were successfully updated.
settings.update_setting_success = Organization settings has been updated successfully.
settings.change_orgname_prompt = This change will affect how links relate to the organization.
settings.update_avatar_success = Organization avatar setting has been updated successfully.
settings.delete = Delete Organization
settings.delete_account = Delete This Organization
settings.delete_prompt = The organization will be permanently removed, and this <strong>CANNOT</strong> be undone!
@@ -713,8 +727,9 @@ authentication = Authentications
config = Configuration
notices = System Notices
monitor = Monitoring
prev = Prev.
next = Next
first_page = First
last_page = Last
total = Total: %d
dashboard.statistic = Statistic
dashboard.operations = Operations
@@ -773,10 +788,13 @@ users.activated = Activated
users.admin = Admin
users.repos = Repos
users.created = Created
users.send_register_notify = Send Registration Notification To User
users.new_success = New account '%s' has been created successfully.
users.edit = Edit
users.auth_source = Authorization Source
users.auth_source = Authentication Source
users.local = Local
users.auth_login_name = Authorization Login Name
users.auth_login_name = Authentication Login Name
users.password_helper = Leave it empty to remain unchanged.
users.update_profile_success = Account profile has been updated successfully.
users.edit_account = Edit Account
users.is_activated = This account is activated
@@ -786,6 +804,7 @@ users.update_profile = Update Account Profile
users.delete_account = Delete This Account
users.still_own_repo = This account still has ownership over at least one repository, you have to delete or transfer them first.
users.still_has_org = This account still has membership in at least one organization, you have to leave or delete the organizations first.
users.deletion_success = Account has been deleted successfully!
orgs.org_manage_panel = Organization Manage Panel
orgs.name = Name
@@ -800,41 +819,47 @@ repos.watches = Watches
repos.stars = Stars
repos.issues = Issues
auths.auth_manage_panel = Authorization Manage Panel
auths.new = Add New Authorization Source
auths.auth_manage_panel = Authentication Manage Panel
auths.new = Add New Source
auths.name = Name
auths.type = Type
auths.enabled = Enabled
auths.updated = Updated
auths.auth_type = Authorization Type
auths.auth_name = Authorization Name
auths.auth_type = Authentication Type
auths.auth_name = Authentication Name
auths.domain = Domain
auths.host = Host
auths.port = Port
auths.bind_dn = Bind DN
auths.bind_password = Bind Password
auths.bind_password_helper = Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base = User Search Base
auths.user_dn = User DN
auths.attribute_name = First name attribute
auths.attribute_surname = Surname attribute
auths.attribute_mail = E-mail attribute
auths.filter = User Filter
auths.admin_filter = Admin Filter
auths.ms_ad_sa = Ms Ad SA
auths.smtp_auth = SMTP Authorization Type
auths.smtp_auth = SMTP Authentication Type
auths.smtphost = SMTP Host
auths.smtpport = SMTP Port
auths.allowed_domains = Allowed Domains
auths.allowed_domains_helper = Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls = Enable TLS Encryption
auths.skip_tls_verify = Skip TLS Verify
auths.pam_service_name = PAM Service Name
auths.enable_auto_register = Enable Auto Registration
auths.tips = Tips
auths.edit = Edit Authorization Setting
auths.activated = This authentication has activated
auths.update_success = Authorization setting has been updated successfully.
auths.update = Update Authorization Setting
auths.delete = Delete This Authorization
auths.delete_auth_title = Authorization Deletion
auths.delete_auth_desc = This authorization is going to be deleted, do you want to continue?
auths.edit = Edit Authentication Setting
auths.activated = This authentication is activate
auths.new_success = New authentication '%s' has been added successfully.
auths.update_success = Authentication setting has been updated successfully.
auths.update = Update Authentication Setting
auths.delete = Delete This Authentication
auths.delete_auth_title = Authentication Deletion
auths.delete_auth_desc = This authentication is going to be deleted, do you want to continue?
auths.deletion_success = Authentication has been deleted successfully!
config.server_config = Server Configuration
config.app_name = Application Name
@@ -858,14 +883,16 @@ config.db_user = User
config.db_ssl_mode = SSL Mode
config.db_ssl_mode_helper = (for "postgres" only)
config.db_path = Path
config.db_path_helper = (for "sqlite3" only)
config.db_path_helper = (for "sqlite3" and "tidb")
config.service_config = Service Configuration
config.register_email_confirm = Require E-mail Confirmation
config.disable_register = Disable Registration
config.show_registration_button = Show Register Button
config.require_sign_in_view = Require Sign In View
config.mail_notify = Mail Notification
config.enable_cache_avatar = Enable Cache Avatar
config.mail_notify = Mail Notification
config.disable_key_size_check = Disable Minimum Key Size Check
config.enable_captcha = Enable Captcha
config.active_code_lives = Active Code Lives
config.reset_password_code_lives = Reset Password Code Lives
config.webhook_config = Webhook Configuration

View File

@@ -1,20 +1,19 @@
app_desc=Un servicio de Git auto alojado y sin complicaciones
home=Incio
home=Inicio
dashboard=Panel de control
explore=Explorar
help=Ayuda
sign_in=Iniciar sesión
social_sign_in=Inicio de sesión social: 2° paso <small>cuenta de asociado</small>
sign_out=Cerrar sesión
sign_up=Suscripción
register=Registro
website=Pagina Web
website=Página Web
version=Versión
page=Página
template=Plantilla
language=Lenguaje
create_new=Crear nuevo...
language=Idioma
create_new=Crear...
user_profile_and_more=Perfil de usuario y más
signed_in_as=Identificado como
@@ -26,7 +25,7 @@ captcha=Captcha
repository=Repositorio
organization=Organización
mirror=Espejo
mirror=Mirror
new_repo=Nuevo repositorio
new_migrate=Nueva Migración
new_fork=Nuevo Fork del Repositorio
@@ -38,9 +37,9 @@ settings=Configuraciones
your_profile=Tu perfil
your_settings=Tu configuración
news_feed=entrada de noticias
pull_requests=Solicitudes de retiro
issues=Publicaciones
news_feed=Feed de noticias
pull_requests=Pull Requests
issues=Incidencias
cancel=Cancelar
@@ -54,25 +53,29 @@ code=Código
[install]
install=Instalación
title=Pasos de la instalación por primera vez
requite_db_desc=Gogs necesita MySQL, PostgreSQL o SQLite3.
docker_helper=Si está ejecutando Gogs usando Docker, por favor lea <a target="_blank" href="%s"> estas pautas</a> antes de cambiar nada en esta página!
requite_db_desc=Gogs requiere una base de datos MySQL, PostgreSQL, SQLite3 o TiDB.
db_title=Configuración de base de datos
db_type=Tipo de base de datos
host=Anfitrión
host=Host
user=Usuario
password=Contraseña
db_name=Nombre de la base de datos
db_helper=Por favor utilice el motor INNODB con la configuración de caracteres utf8_general_ci para MySQL.
ssl_mode=Modo SSL
path=Ruta
sqlite_helper=Ruta del archivo de la base de datos de SQLite3.
err_empty_sqlite_path=La ruta de la base de datos SQLite3 no puede estar vacía.
sqlite_helper=Ruta del archivo con la base de datos SQLite3 o TiDB.
err_empty_db_path=La ruta a la base de datos SQLite3 o TiDB no puede estar vacía.
err_invalid_tidb_name=El nombre de la base de datos TiDB no puede contener los caracteres "." ni "-".
no_admin_and_disable_registration=No puede deshabilitar el registro sin crear una cuenta de administrador.
err_empty_admin_password=La contraseña de administrador no puede estar vacía.
general_title=Configuración General de Gogs
app_name=Nombre de la Aplicación
app_name_helper=Pon aquí el nombre de tu organización, ¡alto y claro!
repo_path=Ruta del repositorio de Raiz (Root)
repo_path_helper=Todos los repositorios remotos de Git se guardarán en este directorio.
run_user=Abrir el usuario
run_user=Ejecutar como Usuario
run_user_helper=El usuario necesita tener acceso a la Ruta Raíz del Repositorio y ejecutar Gogs.
domain=Dominio
domain_helper=Esto afecta a las URLs para clonar por SSH.
@@ -96,9 +99,11 @@ server_service_title=Configuración de Servidor y Otros Servicios
offline_mode=Activar el modo Sin Conexión
offline_mode_popup=Desactivar el CDN incluso en el modo de producción, todos los recursos se servirán localmente.
disable_gravatar=Desactivar el Servicio Gravatar
disable_gravatar_popup=Desactivar Gravatar y cualquier fuente personalizada, todos los avatares deben ser cargados por los usuarios o el avatar por defecto.
disable_gravatar_popup=Desactivar Gravatar y cualquier otra fuente personalizada. Todos los avatares deben ser cargados por los usuarios o en su defecto se mostrará el avatar predeterminado.
disable_registration=Desactivar Auto-Registro
disable_registration_popup=Desactivar auto-registro del usuario, solo el administrador podrá crear cuentas nuevas.
enable_captcha=Activar la Captcha
enable_captcha_popup=Requiere validar la captcha para el auto-registro de usuario.
require_sign_in_view=Activar el Inicio de Sesión obligatorio para Ver Páginas
require_sign_in_view_popup=Solo los usuarios logados pueden ver páginas, los visitantes anónimos solo podrán ver las páginas de login/registro.
admin_setting_desc=No es necesario crear una cuenta de administrador ahora mismo, el usuario que tenga ID=1 obtendrá privilegios de administrador automáticamente.
@@ -127,7 +132,7 @@ my_orgs=Mis Organizaciones
my_mirrors=Mis Mirrors
view_home=Ver %s
issues.in_your_repos=En sus repositorios
issues.in_your_repos=En tus repositorios
[explore]
repos=Repositorios
@@ -143,7 +148,7 @@ forgot_password=He olvidado mi contraseña
forget_password=¿Has olvidado tu contraseña?
sign_up_now=¿Necesitas una cuenta? Regístrate ahora.
confirmation_mail_sent_prompt=Un nuevo correo de confirmación se ha enviado a <b>%s</b>. Por favor, comprueba tu bandeja de entrada en las siguientes %d horas para completar el proceso de registro.
sign_in_email=Inicia sesión con tu correo electrónico
sign_in_to_account=Inicie sesión en su cuenta
active_your_account=Activa tu cuenta
resent_limit_prompt=Lo sentimos, estás solicitando el reenvío del mail de activación con demasiada frecuencia. Por favor, espera 3 minutos.
has_unconfirmed_mail=Hola %s, tu correo electrónico (<b>%s</b>) no está confirmado. Si no has recibido un correo de confirmación o necesitas que lo enviemos de nuevo, por favor, haz click en el siguiente botón.
@@ -155,6 +160,12 @@ invalid_code=Lo sentimos, su código de confirmación ha expirado o no es valido
reset_password_helper=Haga Clic aquí para restablecer su contraseña
password_too_short=La longitud de la contraseña no puede ser menor a 6.
[mail]
activate_account=Por favor, active su cuenta
activate_email=Verifique su correo electrónico
reset_password=Restablezca su contraseña
register_success=Registro completado, bienvenido
[modal]
yes=
no=No
@@ -241,7 +252,7 @@ location=Localización
update_profile=Actualizar Perfil
update_profile_success=Tu perfil se ha actualizado correctamente.
change_username=Nombre de usuario modificado
change_username_desc=El nombre de usuario ha sido modificado, ¿quieres continuar? Esta acción afectará a todos los enlaces relacionados con tu cuenta.
change_username_prompt=Este cambio afectará a los enlaces que hacen referencia a su cuenta.
continue=Continuar
cancel=Cancelar
@@ -256,6 +267,7 @@ update_avatar_success=La configuración de tu avatar se ha actualizado correctam
change_password=Cambiar contraseña
old_password=Contraseña actual
new_password=Nueva contraseña
retype_new_password=Confirmar nueva contraseña
password_incorrect=Contraseña actual incorrecta.
change_password_success=La contraseña se ha modificado correctamente. Ya puedes iniciar sesión con tu nueva contraseña.
@@ -265,15 +277,18 @@ email_desc=Tu dirección de correo principal se utilizará para las notificacion
primary=Principal
primary_email=Marcar como principal
delete_email=Eliminar
email_deletion=Eliminación de Correo Electrónico
email_deletion_desc=Al eliminar esta dirección de correo electrónico se eliminará toda la información asociada a esta dirección de correo electrónico. ¿Deseas continuar?
email_deletion_success=¡El correo electrónico ha sido eliminado correctamente!
add_new_email=Añadir nueva dirección de correo electrónico
add_email=Añadir correo electrónico
add_email_confirmation_sent=Un nuevo correo de confirmación ha sido enviado a <b>%s</b>, por favor, comprueba tu bandeja de entrada en las próximas %d horas para completar el proceso de confirmación.
add_email_confirmation_sent=Un nuevo correo de confirmación ha sido enviado a '%s'. Por favor, comprueba tu bandeja de entrada en las próximas %d horas para completar el proceso.
add_email_success=Tu nuevo correo electrónico se ha añadido correctamente.
manage_ssh_keys=Gestionar Claves SSH
add_key=Añadir Clave
ssh_desc=Esta es la lista de claves SSH asociadas con tu cuenta. Elimina cualquier clave que no reconozcas.
ssh_helper=<strong>¿Necesitas ayuda?</strong>. Consulta nuestra guía para <a href="%s">generar claves SSH</a> o solucionar <a href="%s">problemas comunes de SSH</a>.
ssh_helper=<strong>¿Necesitas ayuda?</strong> Consulta la guía de GitHub para <a href="%s">generar claves SSH</a> o solucionar <a href="%s">problemas comunes</a> al usar SSH.
add_new_key=Añadir clave SSH
ssh_key_been_used=El contenido de la clave pública se ha utilizado.
ssh_key_name_used=Ya existe una clave pública con el mismo nombre.
@@ -281,14 +296,14 @@ key_name=Nombre de la Clave
key_content=Contenido
add_key_success=¡Nueva clave SSH '%s' añadida correctamente!
delete_key=Eliminar
ssh_key_deletion=Borrado de Llave SSH
ssh_key_deletion_desc=Al borrar esta llave SSH no podrá volver a acceder con ella a su cuenta. Desea continuar?
ssh_key_deletion_success=¡La llave SSH ha sido eliminada con éxito!
ssh_key_deletion=Borrado de Clave SSH
ssh_key_deletion_desc=Si elimina esta clave SSH no podrá volver a usarla para acceder a su cuenta. ¿Desea continuar?
ssh_key_deletion_success=¡La clave SSH ha sido eliminada con éxito!
add_on=Añadido en
last_used=Utilizado por última vez en
no_activity=No hay actividad reciente
key_state_desc=Esta clave ha sido usada en los últimos 7 días
token_state_desc=Este token ha sido usado en los últimos 7 días
token_state_desc=Token usado en los últimos 7 días
manage_social=Gestionar Redes Sociales asociadas
social_desc=Esta es una lista de las Redes Sociales asociadas. Elimina cualquier vínculo que no reconozcas.
@@ -297,7 +312,7 @@ unbind_success=La Red Social ha sido desvinculada.
manage_access_token=Gestionar los Tokens de Acceso personales
generate_new_token=Generar nuevo Token
tokens_desc=Tokens generados que se pueden usar para acceder al API de Gogs.
tokens_desc=Tokens usados para acceder al API de Gogs.
new_token_desc=Desde ahora, todos los tokens tendrán acceso completo a tu cuenta.
token_name=Nombre del Token
generate_token=Generar Token
@@ -343,11 +358,11 @@ migrate_type=Tipo de Migración
migrate_type_helper=Este repositorio será un <span class="text blue">mirror</span>
migrate_repo=Migrar Repositorio
migrate.clone_address=Clonar Dirección
migrate.clone_address_desc=Esto puede ser una URL HTTP/HTTPS/GIT o una ruta local del servidor.
migrate.clone_address_desc=Puede ser una URL HTTP/HTTPS/GIT o una ruta local del servidor.
migrate.invalid_local_path=Rutal local inválida, no existe o no es un directorio.
forked_from=forked de
fork_from_self=eres el propietario del repositorio, no puedes hacer fork!
fork_from_self=Eres el propietario del repositorio, ¡no puedes hacer fork!
copy_link=Copiar
click_to_copy=Copiar al portapapeles
copied=Copiado correctamente
@@ -410,19 +425,19 @@ issues.close_tab=%d cerradas
issues.filter_label=Etiqueta
issues.filter_label_no_select=Ninguna etiqueta seleccionada
issues.filter_milestone=Milestone
issues.filter_milestone_no_select=Milestone no seleccionado
issues.filter_assignee=Asignada por
issues.filter_milestone_no_select=Ningún Milestone seleccionado
issues.filter_assignee=Asignada a
issues.filter_assginee_no_select=Sin asignar
issues.filter_type=Tipo
issues.filter_type.all_issues=Todas las incidencias
issues.filter_type.assigned_to_you=Asignada a ti
issues.filter_type.created_by_you=Creada por ti
issues.filter_type.assigned_to_you=Asignadas a ti
issues.filter_type.created_by_you=Creadas por ti
issues.filter_type.mentioning_you=Citado en
issues.filter_sort=Ordenar
issues.filter_sort.latest=Más recientes
issues.filter_sort.oldest=Más antiguos
issues.filter_sort.oldest=Más antiguas
issues.filter_sort.recentupdate=Actualizada recientemente
issues.filter_sort.leastupdate=Least recently updated
issues.filter_sort.leastupdate=Actualizada menos recientemente
issues.filter_sort.mostcomment=Más comentadas
issues.filter_sort.leastcomment=Menos comentadas
issues.opened_by=abierta %[1]s por <a href="%[2]s">%[3]s</a>
@@ -433,7 +448,7 @@ issues.open_title=Abierta
issues.closed_title=Cerrada
issues.num_comments=%d comentarios
issues.commented_at=`comentada <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=No hay contenido por el momento.
issues.no_content=Aun no existe contenido.
issues.close_issue=Cerrar
issues.close_comment_issue=Cerrar y Comentar
issues.reopen_issue=Reabrir
@@ -442,11 +457,11 @@ issues.create_comment=Comentar
issues.closed_at=`cerrada <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reabierta <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`mencionada esta incidencia en un commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.poster=Poster
issues.poster=Autor
issues.admin=Administrador
issues.owner=Propietario
issues.sign_up_for_free=Registro gratuito
issues.sign_in_require_desc=para unirse a esta conversación. Ya dispone de una cuenta? <a href="%s">Inicie sesión para comentar</a>
issues.sign_in_require_desc=para unirse a esta conversación. ¿Ya dispone de una cuenta? <a href="%s">Inicie sesión para comentar</a>
issues.edit=Editar
issues.cancel=Cancelar
issues.save=Guardar
@@ -456,7 +471,7 @@ issues.label_count=%d etiquetas
issues.label_open_issues=%d incidencias abiertas
issues.label_edit=Editar
issues.label_delete=Borrar
issues.label_modify=Modificación de Etiqueta
issues.label_modify=Edición de Etiqueta
issues.label_deletion=Borrado de Etiqueta
issues.label_deletion_desc=Al borrar la etiqueta su información será eliminada de todas las incidencias relacionadas. Desea continuar?
issues.label_deletion_success=Etiqueta borrada con éxito!
@@ -466,10 +481,10 @@ pulls.compare_changes_desc=Comparar dos ramas y generar un pull request con las
pulls.compare_base=base
pulls.compare_compare=comparar con
pulls.filter_branch=Filtrar rama
pulls.no_results=No se han encontrado resultados.
pulls.nothing_to_compare=No hay nada que comparar porque las dos ramas coinciden.
pulls.no_results=Sin resultados.
pulls.nothing_to_compare=Nada que comparar. Las dos ramas coinciden.
pulls.has_pull_request=`Ya existe un pull request entre estas dos ramas: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
pulls.create=Nuevo Pull Request
pulls.create=Crear Pull Request
pulls.title_desc=desea fusionar %[1]d commits de <code>%[2]s</code> en <code>%[3]s</code>
pulls.merged_title_desc=fusionados %[1]d commits de <code>%[2]s</code> en <code>%[3]s</code> %[4]s
pulls.tab_conversation=Conversación
@@ -490,7 +505,7 @@ milestones.close_tab=%d cerradas
milestones.closed=Cerrada %s
milestones.no_due_date=Sin fecha límite
milestones.open=Abrir
milestones.close=Cerrada
milestones.close=Cerrar
milestones.new_subheader=Cree milestones para organizar las incidencias.
milestones.create=Nuevo Milestone
milestones.title=Título
@@ -517,7 +532,7 @@ settings.basic_settings=Configuración Básica
settings.danger_zone=Zona de Peligro
settings.site=Sitio Oficial
settings.update_settings=Actualizar Configuración
settings.change_reponame_prompt=This change will affect how links relate to the repository.
settings.change_reponame_prompt=Este cambio afectará a los enlaces al repositorio.
settings.transfer=Transferir la Propiedad
settings.transfer_desc=Transferir este repositorio a otro usuario u organización donde tengas permisos de administración.
settings.new_owner_has_same_repo=El nuevo propietario tiene un repositorio con el mismo nombre.
@@ -543,7 +558,7 @@ settings.user_is_org_member=El usuario es miembro de la organización, no puede
settings.add_webhook=Añadir Webhook
settings.hooks_desc=Los Webhooks permiten a servicios externos recibir notificaciones cuando sucedan ciertos eventos en Gogs. Cuando sucedan los eventos especificados, enviaremos una petición POST a cada una de las URLs indicadas. Para obtener más información, consulta nuestra <a target="_blank" href="%s">Guía de Webhooks</a>.
settings.webhook_deletion=Eliminar Webhook
settings.webhook_deletion_desc=El borrado de este webhook eliminará su información y todo su historial. ¿Desea continuar?
settings.webhook_deletion_desc=Al borrar este webhook se eliminará su información y todo su historial. ¿Desea continuar?
settings.webhook_deletion_success=¡Webhook eliminado con éxito!
settings.webhook.request=Petición
settings.webhook.response=Respuesta
@@ -567,7 +582,7 @@ settings.event_push_only=Solo el evento <code>push</code>.
settings.event_send_everything=Necesito <strong>todo</strong>.
settings.event_choose=Déjeme elegir lo que necesito.
settings.event_create=Crear
settings.event_create_desc=Branch o etiqueta creada
settings.event_create_desc=Rama o etiqueta creada
settings.event_push=Push
settings.event_push_desc=Git push a un repositorio
settings.active=Activo
@@ -583,16 +598,16 @@ settings.slack_token=Token
settings.slack_domain=Dominio
settings.slack_channel=Canal
settings.deploy_keys=Claves de Despliegue
settings.add_deploy_key=Add Deploy Key
settings.no_deploy_keys=You haven't added any deploy key.
settings.add_deploy_key=Añadir Clave de Despliegue
settings.no_deploy_keys=No has añadido ninguna clave de despliegue.
settings.title=Título
settings.deploy_key_content=Contenido
settings.key_been_used=Deploy key content has been used.
settings.key_name_used=Deploy key with same name has already existed.
settings.add_key_success=New deploy key '%s' has been added successfully!
settings.deploy_key_deletion=Delete Deploy Key
settings.deploy_key_deletion_desc=Delete this deploy key will remove all related accesses for this repository. Do you want to continue?
settings.deploy_key_deletion_success=Deploy key has been deleted successfully!
settings.key_been_used=Se ha usado la clave de despliegue.
settings.key_name_used=Ya existe una clave de despliegue con el mismo nombre.
settings.add_key_success=¡La nueva clave de desplieque '%s' ha sido creada con éxito!
settings.deploy_key_deletion=Eliminar Clave de Despliegue
settings.deploy_key_deletion_desc=Al eliminar esta clave de despliegue se perderán el permiso de acceso a este repositorio con dicha clave. ¿Deseas continuar?
settings.deploy_key_deletion_success=¡Clave de despliegue eliminada con éxito!
diff.browse_source=Explorar el Código
diff.parent=padre
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Ya existe una Release con esta etiqueta.
[org]
org_name_holder=Nombre de la Organización
org_name_helper=Los grandes nombres de organizaciones son cortos y memorables.
org_email_helper=Los correos electrónicos de las organizaciones reciben todas las notificaciones y confirmaciones.
create_org=Crear Organización
repo_updated=Actualizado
people=Personas
@@ -652,12 +666,12 @@ form.name_pattern_not_allowed=El patrón de nombre de la organización '%s' no e
settings=Configuración
settings.options=Opciones
settings.full_name=Nombre Completo
settings.website=Sitio Web
settings.website=Página Web
settings.location=Localización
settings.update_settings=Actualizar Configuración
settings.change_orgname=Nombre de la Organización Modificado
settings.change_orgname_desc=El nombre de la organización ha sido modificado, ¿quieres continuar? Esta acción afectará a todos los enlaces relacionados con esta organización.
settings.update_setting_success=La configuración de la Organización se ha actualizado correctamente.
settings.change_orgname_prompt=Este cambio afectará a los enlaces que hacen referencia a la organización.
settings.update_avatar_success=La configuración de avatar de la organización ha sido actualizada con éxito.
settings.delete=Eliminar Organización
settings.delete_account=Eliminar esta Organización
settings.delete_prompt=Esta operación eliminará esta organización de manera permanente, y ¡<strong>NO PUEDE</strong> deshacerse!
@@ -713,8 +727,9 @@ authentication=Autenticaciones
config=Configuración
notices=Avisos del Sistema
monitor=Monitorización
prev=Anterior
next=Siguiente
first_page=Primera
last_page=Última
total=Total: %d
dashboard.statistic=Estadísticas
dashboard.operations=Operaciones
@@ -773,10 +788,13 @@ users.activated=Activado
users.admin=Administrador
users.repos=Repositorios
users.created=Creado
users.send_register_notify=Send Registration Notification To User
users.new_success=La cuenta '%s' ha sido creada con éxito.
users.edit=Editar
users.auth_source=Origen de Autorización
users.auth_source=Fuente de Autenticación
users.local=Local
users.auth_login_name=Nombre de Usuario de Autorización
users.auth_login_name=Nombre de Inicio de Sesión de Autenticación
users.password_helper=Deje el campo vacío si no desea cambiar la contraseña.
users.update_profile_success=El perfil de la cuenta se ha actualizado correctamente.
users.edit_account=Editar Cuenta
users.is_activated=Esta cuenta está activada
@@ -786,6 +804,7 @@ users.update_profile=Actualizar Perfil de la Cuenta
users.delete_account=Eliminar esta Cuenta
users.still_own_repo=Esta cuenta es propietaria de uno o más repositorios, tienes que borrarlos o transferirlos primero.
users.still_has_org=Esta cuenta es miembro de una o más organizaciones, tienes que abandonarlas o eliminarlas primero.
users.deletion_success=Su cuenta ha sido eliminada correctamente!
orgs.org_manage_panel=Panel de Gestión de Organización
orgs.name=Nombre
@@ -800,41 +819,47 @@ repos.watches=Vigilantes
repos.stars=Estrellas
repos.issues=Incidencias
auths.auth_manage_panel=Panel de Gestión de Autorizaciones
auths.new=Añadir nuevo origen de autorización
auths.auth_manage_panel=Panel de Administración de Autenticación
auths.new=Añadir Nuevo Origen
auths.name=Nombre
auths.type=Tipo
auths.enabled=Activo
auths.updated=Actualizado
auths.auth_type=Tipo de Autorización
auths.auth_name=Nombre de Autorización
auths.auth_type=Tipo de Autenticación
auths.auth_name=Nombre de Autenticación
auths.domain=Dominio
auths.host=Host
auths.port=Puerto
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.user_base=User Search Base
auths.bind_password=Contraseña Bind
auths.bind_password_helper=Advertencia: La contraseña se almacena como texto plano. No utilice una cuenta con privilegios elevados.
auths.user_base=Base de Búsqueda de Usuarios
auths.user_dn=DN de Usuario
auths.attribute_name=Atributo nombre
auths.attribute_surname=Atributo apellido
auths.attribute_mail=Atributo correo electrónico
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.filter=Filtro de Usuario
auths.admin_filter=Filtro de Aministrador
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Tipo de Autorización SMTP
auths.smtp_auth=Tipo de Autenticación SMTP
auths.smtphost=SMTP Host
auths.smtpport=Puerto SMTP
auths.allowed_domains=Dominios Permitidos
auths.allowed_domains_helper=Deje el campo vacío si no desea restringir ningún dominio. Para restringir más de uno, separe los dominios con una coma ','.
auths.enable_tls=Habilitar Cifrado TLS
auths.skip_tls_verify=Omitir la verificación TLS
auths.pam_service_name=Nombre del Servicio PAM
auths.enable_auto_register=Hablilitar Auto-Registro
auths.tips=Consejos
auths.edit=Editar la Configuración de Autorización
auths.edit=Editar la Configuración de Autenticación
auths.activated=Esta autenticación ha sido activada
auths.update_success=La Configuración de Autorización ha sido actualizada correctamente.
auths.update=Actualizar la Configuración de Autorización
auths.delete=Eliminar esta Autorización
auths.delete_auth_title=Eliminación de Autorización
auths.delete_auth_desc=Se va a eliminar esta autorización, ¿quieres continuar?
auths.new_success=¡La autenticación '%s' ha sido añadida con éxito!
auths.update_success=La configuración de autenticación ha sido actualizada con éxito.
auths.update=Actualizar la Configuración de Autenticación
auths.delete=Eliminar Autenticación
auths.delete_auth_title=Borrado de Autenticación
auths.delete_auth_desc=Esta autenticación será eliminada. ¿Deseas continuar?
auths.deletion_success=¡La autenticación ha sido eliminada con éxito!
config.server_config=Configuración del Servidor
config.app_name=Nombre de la Aplicación
@@ -843,7 +868,7 @@ config.app_url=URL de la Aplicación
config.domain=Dominio
config.offline_mode=Modo Sin Conexión
config.disable_router_log=Deshabilitar Log del Router
config.run_user=Usuario de Ejecución
config.run_user=Ejecutada como Usuario
config.run_mode=Modo de Ejecución
config.repo_root_path=Ruta del Repositorio
config.static_file_root_path=Ruta de los Ficheros Estáticos
@@ -858,14 +883,16 @@ config.db_user=Usuario
config.db_ssl_mode=Modo SSL
config.db_ssl_mode_helper=(solo para "postgres")
config.db_path=Ruta
config.db_path_helper=(solo para "sqlite3")
config.db_path_helper=(para "sqlite3" y "tidb")
config.service_config=Configuración del Servicio
config.register_email_confirm=Solicitar Confirmación por Correo Electrónico
config.disable_register=Deshabilitar el Registro
config.show_registration_button=Mostrar Botón de Registro
config.require_sign_in_view=Solicitar la Vista de Inicio de Sesión
config.mail_notify=Notificación por Correo Electrónico
config.enable_cache_avatar=Activar la Caché de Avatar
config.mail_notify=Notificación por Correo Electrónico
config.disable_key_size_check=Deshabilitar la comprobación de Tamaño Mínimo de Clave
config.enable_captcha=Activar Captcha
config.active_code_lives=Habilitar Vida del Código
config.reset_password_code_lives=Restablecer Contraseña de Vida del Código
config.webhook_config=Configuración de Webhooks
@@ -918,7 +945,7 @@ notices.op=Op.
notices.delete_success=La notificación del sistema se ha eliminado correctamente.
[action]
create_repo=Repositorio creado <a href="%s">%s</a>
create_repo=repositorio creado <a href="%s">%s</a>
rename_repo=repositorio renombrado de <code>%[1]s</code> a <a href="%[2]s">%[3]s</a>
commit_repo=hizo push a <a href="%s/src/%s">%[2]s</a> en <a href="%[1]s">%[3]s</a>
create_issue=`incidencia abierta <a href="%s/issues/%s">%s#%[2]s</a>`

View File

@@ -5,7 +5,6 @@ dashboard=Tableau de bord
explore=Explorer
help=Aide
sign_in=Connexion
social_sign_in=Connexion avec un réseau social : 2e étape <small>associer un compte</small>
sign_out=Déconnexion
sign_up=Créer un compte
register=S'enregistrer
@@ -14,7 +13,7 @@ version=Version
page=Page
template=Modèle
language=Langue
create_new=Créer nouveau...
create_new=Create...
user_profile_and_more=Profil utilisateur et plus
signed_in_as=Connecté en tant que
@@ -54,7 +53,8 @@ code=Code
[install]
install=Installation
title=Instructions de Première Installation
requite_db_desc=Gogs nécessite MySQL, PostgreSQL ou SQLite3.
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=Paramètres de la base de données
db_type=Type de Base de Données
host=Hôte
@@ -64,8 +64,11 @@ db_name=Nom de la Base de Données
db_helper=Veuillez utiliser le moteur INNODB avec le jeu de caractères utf8_general_ci pour MySQL.
ssl_mode=Mode SSL
path=Chemin
sqlite_helper=Emplacement du fichier de la base de données SQLite3.
err_empty_sqlite_path=Le chemin de la base de donnée SQLite3 ne peut être vide.
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=Paramètres Généraux de Gogs
app_name=Nom de l'Application
@@ -99,6 +102,8 @@ disable_gravatar=Disable Gravatar Service
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration=Désactiver le formulaire d'inscription
disable_registration_popup=Désactiver le formulaire d'inscription, seuls les administrateurs peuvent créer des comptes.
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Demander une connexion pour afficher des pages
require_sign_in_view_popup=Seules les personnes connectées peuvent voir les pages. Les visiteurs anonymes ne pourront voir que les pages de connexion/enregistrement.
admin_setting_desc=Vous n'avez pas besoin de créer un compte admin. L'utilisateur ayant l'ID = 1 aura l'accès admin automatiquement.
@@ -143,7 +148,7 @@ forgot_password=Mot de Passe oublié
forget_password=Mot de Passe oublié ?
sign_up_now=Pas de compte ? Créer maintenant.
confirmation_mail_sent_prompt=Un nouveau mail de confirmation à été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans un délai de %d heures pour compléter votre enregistrement.
sign_in_email=Connexion avec l'E-mail
sign_in_to_account=Sign in to your account
active_your_account=Activer votre Compte
resent_limit_prompt=Désolé, vos tentatives d'activation sont trop fréquentes. Veuillez réessayer dans 3 minutes.
has_unconfirmed_mail=Bonjour %s, votre adresse courriel (<b>%s</b>) n'a pas été confirmée. Si vous n'avez reçu aucun courriel de confirmation ou souhaitez renouveler l'envoi, appuyez sur le bouton ci-dessous.
@@ -155,6 +160,12 @@ invalid_code=Désolé, code de confirmation invalide ou expiré.
reset_password_helper=Cliquez ici pour réinitialiser votre mot de passe
password_too_short=Le mot de passe doit contenir 6 caractères minimum.
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=Oui
no=Non
@@ -241,7 +252,7 @@ location=Localisation
update_profile=Valider les modifications
update_profile_success=Profil mis à jour avec succès.
change_username=Non d'utilisateur modifié
change_username_desc=Nom d'utilisateur modifié. Cela affecte tous les liens relatifs à votre compte. Continuer ?
change_username_prompt=This change will affect the way how links relate to your account.
continue=Continuer
cancel=Annuler
@@ -256,6 +267,7 @@ update_avatar_success=Votre avatar a été mis à jour avec succès.
change_password=Modifier le Mot de Passe
old_password=Mot de Passe actuel
new_password=Nouveau Mot de Passe
retype_new_password=Retype New Password
password_incorrect=Mot de passe actuel incorrect.
change_password_success=Mot de passe modifié avec succès. Vous pouvez à présent vous connecter avec le nouveau mot de passe.
@@ -265,9 +277,12 @@ email_desc=Votre adresse e-mail principale sera utilisée pour les notifications
primary=Principale
primary_email=Définir comme principale
delete_email=Supprimer
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=Ajouter une nouvelle adresse courriel
add_email=Ajouter un courriel
add_email_confirmation_sent=Un nouvel e-mail de confirmation a été envoyé à <b>%s</b>, merci de vérifier votre boite de réception dans les %d heures pour compléter le processus de confirmation.
add_email_confirmation_sent=Une nouvelle confirmation d'adresse e-mail a été envoyé à '%s', veuillez vérifier votre boîte de réception dans un délai de %d heures pour terminer le processus de confirmation.
add_email_success=Votre courriel a été ajouté avec succès.
manage_ssh_keys=Gérer les clés SSH
@@ -330,7 +345,7 @@ license=Licence
license_helper=Sélectionner un fichier de licence
readme=Readme
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
auto_init=Initialize this repository with selected files and template
create_repo=Créer un Référentiel
default_branch=Branche par défaut
mirror_interval=Intervalle du miroir (heure)
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Une publication avec ce nom de tag a déjà exist
[org]
org_name_holder=Nom d'organisation
org_name_helper=Idéalement, un nom d'organisation devrait être court et mémorable.
org_email_helper=Le courriel de l'organisation recevra toutes les notifications et confirmations.
create_org=Créer une organisation
repo_updated=Mis à jour
people=Contacts
@@ -655,9 +669,9 @@ settings.full_name=Non Complet
settings.website=Site Web
settings.location=Localisation
settings.update_settings=Valider
settings.change_orgname=Organisation renommée
settings.change_orgname_desc=L'Organisation a été renommée. Cela affecte tous les liens relatifs à cette organisation. Continuer ?
settings.update_setting_success=Paramètres d'organisation modifiés avec succès.
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=Supprimer l'organisation
settings.delete_account=Supprimer cette organisation
settings.delete_prompt=Cela supprimera cette organisation définitivement. Cette opération est <strong>IRRÉVERSIBLE</strong> !
@@ -713,8 +727,9 @@ authentication=Authentifications
config=Configuration
notices=Notes Systèmes
monitor=Supervision
prev=Préc.
next=Suiv.
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=Statistiques
dashboard.operations=Opérations
@@ -773,10 +788,13 @@ users.activated=Activés
users.admin=Administrateur
users.repos=Dépôts
users.created=Créés
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=Éditer
users.auth_source=Source d'Autorisation
users.auth_source=Authentication Source
users.local=Locales
users.auth_login_name=Autorisation de connexion
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=Profil mis à jour avec succès.
users.edit_account=Modifier le Compte
users.is_activated=Ce compte est activé
@@ -786,6 +804,7 @@ users.update_profile=Mettre le profil à jour
users.delete_account=Supprimer ce Compte
users.still_own_repo=Ce compte possède toujours des dépôts. Vous devez d'abord les supprimer ou les transférer.
users.still_has_org=Ce compte a toujours membres de l'organisation, vous avez à gauche ou supprimez tout d'abord.
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=Gestion des Organisations
orgs.name=Nom
@@ -800,41 +819,47 @@ repos.watches=Suivi par
repos.stars=Votes
repos.issues=Problèmes
auths.auth_manage_panel=Gestion des Autorisations
auths.new=Ajouter une Nouvelle Source d'Autorisation
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=Nom
auths.type=Type
auths.enabled=Activé
auths.updated=Mis à jour
auths.auth_type=Type d'Autorisation
auths.auth_name=Nom d'Autorisation
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=Domaine
auths.host=Hôte
auths.port=Port
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=Attribut du prénom
auths.attribute_surname=Attribut du nom de famille
auths.attribute_mail=Attribut de l'e-mail
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Type d'Autorisation SMTP
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=Hôte SMTP
auths.smtpport=Port SMTP
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=Activer le Chiffrement TLS
auths.skip_tls_verify=Skip TLS Verify
auths.pam_service_name=Nom du Service PAM
auths.enable_auto_register=Connexion Automatique
auths.tips=Conseils
auths.edit=Modifier les Paramètres d'Autorisation
auths.edit=Edit Authentication Setting
auths.activated=Authentification activée
auths.update_success=Paramètres d'autorisation mis à jour avec succès.
auths.update=Mettre les Paramètres d'Autorisation à jour
auths.delete=Supprimer cette Autorisation
auths.delete_auth_title=Suppression d'Autorisation
auths.delete_auth_desc=Cette autorisation sera supprimée. Continuer ?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=Configuration du Serveur
config.app_name=Nom de l'Application
@@ -858,14 +883,16 @@ config.db_user=Utilisateur
config.db_ssl_mode=Mode SSL
config.db_ssl_mode_helper=("postgres" uniquement)
config.db_path=Emplacement
config.db_path_helper=("sqlite3" uniquement)
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=Configuration du Service
config.register_email_confirm=Nécessite une confirmation par courriel
config.disable_register=Désactiver l'Enregistrement
config.show_registration_button=Afficher le bouton d'enregistrement
config.require_sign_in_view=Connexion Obligatoire pour Visualiser
config.mail_notify=Mailer les Notifications
config.enable_cache_avatar=Activer le Cache d'Avatar
config.mail_notify=Mailer les Notifications
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=Limites de Code Actif
config.reset_password_code_lives=Réinitialiser le Mot De Passe des Limites de Code
config.webhook_config=Configuration Webhook

View File

@@ -5,7 +5,6 @@ dashboard=Pannello di controllo
explore=Esplora
help=Aiuto
sign_in=Accedi
social_sign_in=Login Sociale: Passo 2 <small>associare l'account</small>
sign_out=Esci
sign_up=Registrati
register=Registrati
@@ -14,7 +13,7 @@ version=Versione
page=Pagina
template=Template
language=Lingua
create_new=Create new...
create_new=Crea...
user_profile_and_more=User profile and more
signed_in_as=Signed in as
@@ -35,8 +34,8 @@ manage_org=Gestisci le organizzazioni
admin_panel=Pannello di amministrazione
account_settings=Impostazioni dell'account
settings=Impostazioni
your_profile=Your Profile
your_settings=Your Settings
your_profile=Profilo
your_settings=Impostazioni
news_feed=Notizie
pull_requests=Pull Requests
@@ -54,7 +53,8 @@ code=Codice
[install]
install=Installazione
title=Passi d'installazione per il primo avvio
requite_db_desc=Gogs richiede MySql, PostgresSQL o SQLite.
docker_helper=Se stai utilizzando Gogs su Docker, per favore leggi le <a target="_blank" href="%s">Linee guida</a> con attenzione prima di cambiare qualcosa su questa pagina!
requite_db_desc=Gogs necessita MySQL, PostgreSQL, SQLite3 o TiDB.
db_title=Impostazioni Database
db_type=Tipo di database
host=Host
@@ -64,8 +64,11 @@ db_name=Nome del database
db_helper=Utilizza il motore INNODB con codifica utf8_general_ci per MySQL.
ssl_mode=Modalità SSL
path=Percorso
sqlite_helper=Percorso per database SQLite3.
err_empty_sqlite_path=Il percorso del database SQLite3 non può essere vuoto.
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=Impostazioni di Base dell'Applicazione
app_name=Nome Applicazione
@@ -76,7 +79,7 @@ run_user=Esegui con l'utente
run_user_helper=L'utente deve avere accesso al percorso root del repository e avviare Gogs.
domain=Dominio
domain_helper=Questo modifica lo SSH clone URLs.
ssh_port=SSH Port
ssh_port=Porta SSH
ssh_port_helper=Port number which your SSH server is using, leave it empty to disable SSH feature.
http_port=Porta HTTP
http_port_helper=Porta di ascolto dell'applicazione.
@@ -99,6 +102,8 @@ disable_gravatar=Disable Gravatar Service
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration=Disabilita Registrazione Manuale
disable_registration_popup=Disabilita la registrazione manuale degli utenti, solo gli amministratori possono creare account.
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Abilita Richiesta di Accesso per Vedere le Pagine
require_sign_in_view_popup=Solo gli utenti loggati possono vedere le pagine, i visitatori potranno vedere solo le pagine di accesso e registrazione.
admin_setting_desc=Non devi per forza creare un account admin proprio adesso, ma comunque l'utente ID=1 otterrà l'accesso da amministratore automaticamente.
@@ -143,7 +148,7 @@ forgot_password=Password dimenticata
forget_password=Password dimenticata?
sign_up_now=Bisogno di un account? Iscriviti ora.
confirmation_mail_sent_prompt=Una nuova email di conferma è stata inviata a <b>%s</b>, verifica la tua casella di posta entro le prossime %d ore per completare la registrazione.
sign_in_email=Accedi al tuo indirizzo e-mail
sign_in_to_account=Sign in to your account
active_your_account=Attiva il tuo Account
resent_limit_prompt=Siamo spiacenti, si stanno inviando e-mail di attivazione troppo spesso. Si prega di attendere 3 minuti.
has_unconfirmed_mail=Ciao %s, hai un indirizzo di posta elettronica non confermato (<b>%s</b>). Se non hai ricevuto una e-mail di conferma o vuoi riceverla nuovamente, fare clic sul pulsante qui sotto.
@@ -155,6 +160,12 @@ invalid_code=Siamo spiacenti, il codice di conferma è scaduto o non valido.
reset_password_helper=Clicca qui per reimpostare la password
password_too_short=La lunghezza della password non può essere meno 6 caratteri.
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=
no=No
@@ -241,7 +252,7 @@ location=Posizione
update_profile=Aggiorna Profilo
update_profile_success=Il tuo profilo è stato aggiornato con successo.
change_username=Username Cambiato
change_username_desc=Hai cambiato il tuo username. Questo influenzerà il modo in cui i link si relazionano al tuo account. Vuoi proseguire?
change_username_prompt=This change will affect the way how links relate to your account.
continue=Continua
cancel=Annulla
@@ -256,6 +267,7 @@ update_avatar_success=Le tue impostazioni avatar sono state aggiornate con succe
change_password=Cambia Password
old_password=Password attuale
new_password=Nuova Password
retype_new_password=Retype New Password
password_incorrect=La Password attuale non è corretta.
change_password_success=La tua password è stata cambiata con successo. Ora puoi accedere usando la nuova password.
@@ -265,9 +277,12 @@ email_desc=Il tuo indirizzo e-mail primario sarà usato per le notifiche e altre
primary=Primario
primary_email=Imposta come primario
delete_email=Elimina
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=Aggiungi un nuovo indirizzo E-mail
add_email=Aggiungi E-mail
add_email_confirmation_sent=Una nuova email di conferma è stata inviata a <b>%s</b>, per favore controlla la tua posta in arrivo nelle prossime %d ore per completare il processo di registrazione.
add_email_confirmation_sent=Una nuova email di conferma è stata inviata a '%s', per favore controlla la tua posta in arrivo nelle prossime %d ore per completare il processo di registrazione.
add_email_success=Il tuo nuovo indirizzo e-mail è stato aggiunto con successo.
manage_ssh_keys=Gestisci chiavi SSH
@@ -287,7 +302,7 @@ ssh_key_deletion_success=SSH key has been deleted successfully!
add_on=Aggiunto il
last_used=Ultimo accesso il
no_activity=Nessuna attività recente
key_state_desc=This key is used in last 7 days
key_state_desc=Hai utilizzato questa chiave negli ultimi 7 giorni
token_state_desc=This token is used in last 7 days
manage_social=Gestisci gli Account Sociali Associati
@@ -325,12 +340,12 @@ fork_from=Forka da
fork_visiblity_helper=Non puoi cambiare la visibilità di un repository forkato.
repo_desc=Descrizione
repo_lang=Lingua
repo_lang_helper=Select .gitignore files
repo_lang_helper=Seleziona file .gitignore
license=Licenza
license_helper=Selezionare un file di licenza
readme=Readme
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
readme_helper=Seleziona un template per il readme
auto_init=Initialize this repository with selected files and template
create_repo=Crea Repository
default_branch=Ramo (Branch) predefinito
mirror_interval=Intervallo Mirror (in ore)
@@ -469,7 +484,7 @@ pulls.filter_branch=Filter branch
pulls.no_results=No results found.
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even.
pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
pulls.create=Create Pull Request
pulls.create=Crea Pull Request
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code>
pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
pulls.tab_conversation=Conversation
@@ -482,7 +497,7 @@ pulls.data_broken=Data of this pull request has been broken due to deletion of f
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it.
pulls.merge_pull_request=Merge Pull Request
pulls.merge_pull_request=Unisci Pull Request
milestones.new=New Milestone
milestones.open_tab=%d Open
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Un rilascio con questo tag esiste già.
[org]
org_name_holder=Nome dell'Organizzazione
org_name_helper=Le migliori organizzazioni hanno nomi brevi e memorabili.
org_email_helper=L'Email dell'organizzazione riceve tutte le notifiche e le conferme.
create_org=Crea Organizzazione
repo_updated=Aggiornato
people=Utenti
@@ -655,9 +669,9 @@ settings.full_name=Nome Completo
settings.website=Sito Web
settings.location=Residenza
settings.update_settings=Aggiorna Impostazioni
settings.change_orgname=Il nome dell'organizzazione è cambiato
settings.change_orgname_desc=Il nome dell'organizzazione name è cambiato. Questo influenzerà come collegamenti si riferiscono all'organizzazione. Si desidera continuare?
settings.update_setting_success=Impostazioni dell'organizzazione aggiornate con successo.
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=Elimina organizzazione
settings.delete_account=Elimina questa organizzazione
settings.delete_prompt=L'organizzazione verrà rimossa definitivamente, e questa operazione <strong>NON PUÒ</strong> essere annullata!
@@ -713,8 +727,9 @@ authentication=Autenticazioni
config=Configurazione
notices=Avvisi di sistema
monitor=Monitoraggio
prev=Prec.
next=Succ.
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=Statistiche
dashboard.operations=Operazioni
@@ -773,10 +788,13 @@ users.activated=Attivato
users.admin=Amministratore
users.repos=Repo
users.created=Creato
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=Modifica
users.auth_source=Origine Autorizzazione
users.auth_source=Authentication Source
users.local=Locale
users.auth_login_name=Nome di Accesso dell'Autorizzazione
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=Profilo dell'account aggiornato con successo.
users.edit_account=Modifica Account
users.is_activated=Questo account è attivato
@@ -786,6 +804,7 @@ users.update_profile=Aggiornare Profilo Account
users.delete_account=Elimina Questo Account
users.still_own_repo=Questo account possiede ancora almeno un repository, devi prima cancellarli o trasferirli.
users.still_has_org=Questo account appartiene ancora ad almeno un'organizzazione, è necessario prima abbandonarle o eliminale.
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=Pannello Gestione Organizzazioni
orgs.name=Nome
@@ -800,41 +819,47 @@ repos.watches=Segue
repos.stars=Voti
repos.issues=Problemi
auths.auth_manage_panel=Pannello Gestione Autorizzazioni
auths.new=Aggiungi Nuova Origine Autorizzazione
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=Nome
auths.type=Tipo
auths.enabled=Attivo
auths.updated=Aggiornato
auths.auth_type=Tipo di Autorizzazione
auths.auth_name=Nome Autorizzazione
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=Dominio
auths.host=Host
auths.port=Porta
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=Attributo Nome
auths.attribute_surname=Attributo Cognome
auths.attribute_mail=Attributo Email
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Tipo di Autorizzazione SMTP
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=Host SMTP
auths.smtpport=Porta SMTP
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=Abilitare Crittografia TLS
auths.skip_tls_verify=Skip TLS Verify
auths.pam_service_name=Nome del Servizio PAM
auths.enable_auto_register=Abilitare Registrazione Automatica
auths.tips=Consigli
auths.edit=Modificare Impostazioni Autorizzazioni
auths.edit=Edit Authentication Setting
auths.activated=Questa Autenticazione è stata attivata
auths.update_success=Impostazione dell'Autorizzazione aggiornate con successo.
auths.update=Aggiorna Impostazioni Autorizzazioni
auths.delete=Elimina Questa Autorizzazione
auths.delete_auth_title=Eliminazione Autorizzazione
auths.delete_auth_desc=Questa autorizzazione sarà cancellata, vuoi continuare?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=Configurazione Server
config.app_name=Nome Applicazione
@@ -858,14 +883,16 @@ config.db_user=Utente
config.db_ssl_mode=Modalità SSL
config.db_ssl_mode_helper=(solo per "postgres")
config.db_path=Percorso
config.db_path_helper=(solo per "sqlite3")
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=Configurazione Servizio
config.register_email_confirm=Richiedono Conferma dell'Email
config.disable_register=Disabilita Registrazione
config.show_registration_button=Mostra Pulsane Registrazione
config.require_sign_in_view=Richiesto Accesso per Vedere
config.mail_notify=Email di Notifica
config.enable_cache_avatar=Abilitare Cache dell'Avatar
config.mail_notify=Email di Notifica
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=Attiva Vita del Codice
config.reset_password_code_lives=Reimpostare Password della Vita del Codice
config.webhook_config=Configurazione Webhook
@@ -922,7 +949,7 @@ create_repo=ha creato il repository <a href="%s">%s</a>
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
commit_repo=ha pushato nel <a href="%s/src/%s">%[2]s</a> in <a href="%[1]s">%[3]s</a>
create_issue=`ha aperto il problema <a href="%s/issues/%s">%s#%[2]s</a>`
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
create_pull_request=`creata pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
comment_issue=`ha commentato il problema <a href="%s/issues/%s">%s#%[2]s</a>`
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
transfer_repo=ha trasferito il repository <code>%s</code> a <a href="%s">%s</a>

View File

@@ -5,7 +5,6 @@ dashboard=ダッシュボード
explore=エスクプローラ
help=ヘルプ
sign_in=サインイン
social_sign_in=SNSでサインイン: ステップ2 <small>アカウント連携</small>
sign_out=サインアウト
sign_up=サインアップ
register=登録
@@ -14,7 +13,7 @@ version=バージョン
page=ページ
template=テンプレート
language=言語
create_new=新規作成...
create_new=Create...
user_profile_and_more=ユーザープロファイルなど
signed_in_as=サインイン済み
@@ -54,7 +53,8 @@ code=コード
[install]
install=インストール
title=初回実行のインストール手順
requite_db_desc=Gogs には、MySQL や PostgreSQL 、SQLite3 が必要です。
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=データベース設定
db_type=データベースの種類
host=ホスト
@@ -64,8 +64,11 @@ db_name=データベース名
db_helper=Mysql INNODB エンジン utf8_general_ci の文字セットを使用してください。
ssl_mode=SSL モード
path=パス
sqlite_helper=SQLite3 データベースのファイル パス
err_empty_sqlite_path=SQLite3 データベースのパスは、空にすることはできません。
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=Gogs の全般設定
app_name=アプリケーション名
@@ -76,7 +79,7 @@ run_user=実行ユーザ
run_user_helper=ユーザーはリポジトリ ルートパスへのアクセス、及びGogs を実行する権限を所有する必要があります。
domain=ドメイン
domain_helper=これはSSHクローンURLに影響する。
ssh_port=SSH Port
ssh_port=SSH ポート
ssh_port_helper=Port number which your SSH server is using, leave it empty to disable SSH feature.
http_port=HTTP ポート
http_port_helper=アプリケーションが待ち受けするポート番号。
@@ -95,10 +98,12 @@ mail_notify=メール通知を有効にする
server_service_title=サーバーとその他のサービスの設定
offline_mode=オフラインモード有効化
offline_mode_popup=プロダクション モードでCDN を無効にし、すべてのリソースファイルをローカルで提供します 。
disable_gravatar=Disable Gravatar Service
disable_gravatar=Gravatarのサービスを無効にします
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration=自己登録を無効にする
disable_registration_popup=自己登録を無効にし、管理者のみがアカウント作成できる
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=サインインしたユーザのみページ閲覧を許可
require_sign_in_view_popup=サインインしたユーザのみがページを閲覧できます。ビジターはサインインもしくはサインアップページのみ見られます。
admin_setting_desc=今管理者アカウントを作成する必要はありません。ID = 1のユーザ は自動的に管理者の権限を獲得します。
@@ -125,9 +130,9 @@ my_repos=私のリポジトリ
collaborative_repos=共同リポジトリ
my_orgs=私の組織
my_mirrors=私のミラー
view_home=View %s
view_home=ビュー %s
issues.in_your_repos=In your repositories
issues.in_your_repos=あなたのリポジトリ
[explore]
repos=リポジトリ
@@ -143,7 +148,7 @@ forgot_password=パスワードを忘れた
forget_password=パスワードを忘れた?
sign_up_now=アカウントが必要ですか?今すぐサインアップ
confirmation_mail_sent_prompt=新しい確認メールを <b>%s</b> に送りました。登録を完了させるために、%d時間以内にあなたのメールボックスを確認してください。
sign_in_email=E-mailでサイイン
sign_in_to_account=Sign in to your account
active_your_account=アカウントをアクティブ
resent_limit_prompt=申し訳ありませんが、アクティベーションメールは頻繁に送信しています。3 分お待ちください。
has_unconfirmed_mail=こんにちは %s さん、あなたの電子メール アドレス (<b>%s</b>) は未確認です。もし確認メールをまだ確認できていないか、改めて再送信する場合は、下のボタンをクリックしてください。
@@ -155,6 +160,12 @@ invalid_code=申し訳ありませんが、確認用コードが期限切れま
reset_password_helper=パスワードをリセットするにはここをクリック
password_too_short=6文字未満のパスワードは設定できません。
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=はい
no=いいえ
@@ -241,7 +252,7 @@ location=ロケーション
update_profile=プロファイル更新
update_profile_success=あなたのプロフィールが更新されました。
change_username=ユーザー名が変更されました
change_username_desc=ユーザー名が変更されている、継続したいですか?これはあなたのアカウントに関連するすべてのリンクに影響を与える。
change_username_prompt=This change will affect the way how links relate to your account.
continue=続行
cancel=キャンセル
@@ -256,6 +267,7 @@ update_avatar_success=あなたのアバターの設定が更新されました
change_password=パスワードを変更
old_password=現在のパスワード
new_password=新しいパスワード
retype_new_password=Retype New Password
password_incorrect=現在のパスワードが正しくありません。
change_password_success=パスワードが正常に変更されました。今すぐ新しいパスワード経由でサインインすることができます。
@@ -265,9 +277,12 @@ email_desc=あなたのプライマリメールアドレスは、通知やその
primary=プライマリー
primary_email=プライマリに設定
delete_email=削除
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=新しいe-mailアドレスを追加
add_email=電子メールを追加します。
add_email_confirmation_sent=<b>%s</b> に新しい確認メールを送信しました、次の %d 時間以内に受信トレイを確認し、確認プロセスを完了してください。
add_email_confirmation_sent='%s' に新しい確認メールを送信しました、次の %d 時間以内に受信トレイを確認し、確認プロセスを完了してください。
add_email_success=新しいe-mail アドレスが追加されました。
manage_ssh_keys=SSH キーを管理
@@ -281,14 +296,14 @@ key_name=キーの名前
key_content=コンテンツ
add_key_success=新しいSSHキー '%s' が正常に追加されました!
delete_key=削除
ssh_key_deletion=SSH Key Deletion
ssh_key_deletion_desc=Delete this SSH key will remove all related accesses for your account. Do you want to continue?
ssh_key_deletion_success=SSH key has been deleted successfully!
ssh_key_deletion=SSH キーの削除
ssh_key_deletion_desc=このSSHキーを削除すると、あなたのアカウントに関連するすべてのアクセスが削除されます。続行しますか
ssh_key_deletion_success=SSH キーは正常に削除されました!
add_on=追加された
last_used=最終使用日
no_activity=最近の活動なし
key_state_desc=この鍵は7日間以内に使われています。
token_state_desc=This token is used in last 7 days
token_state_desc=この鍵は7日間以内に使われています。
manage_social=関連付けられているSNSアカウントを管理
social_desc=これは関連付けられたソーシャルアカウントのリストです。あなたが認識していない結び付けを削除します。
@@ -297,15 +312,15 @@ unbind_success=SNSアカウントがバインドされていない。
manage_access_token=個人のアクセス トークンを管理
generate_new_token=新しいトークンを生成
tokens_desc=Tokens you have generated that can be used to access the Gogs APIs.
tokens_desc=生成したトークンを利用して Gogs の API にアクセスすることができます。
new_token_desc=今のところ、全てのトークンはあなたのアカウントにフルアクセスできます。
token_name=トークン名
generate_token=トークンを生成
generate_token_succees=新しいアクセス トークンは正常に生成されました !今すぐあなたの新しいアクセス トークンをコピーしておいてください。二度と見ることはできませんので確認してください!
delete_token=削除
access_token_deletion=Personal Access Token Deletion
access_token_deletion_desc=Delete this personal access token will remove all related accesses of application. Do you want to continue?
delete_token_success=Personal access token has been removed successfully! Don't forget to update your application as well.
access_token_deletion=パーソナルアクセストークンの削除
access_token_deletion_desc=パーソナルアクセストークンを削除すると、関連するアプリケーションのすべてのアクセスが削除されます。続行しますか?
delete_token_success=パーソナルアクセストークンは正常に削除されました!同時にあなたのアプリケーションを更新することを忘れないでください。
delete_account=アカウントを削除
delete_prompt=この操作はあなたのアカウントを完全に削除し、復旧<strong>できない</strong>
@@ -319,18 +334,18 @@ repo_name=リポジトリ名
repo_name_helper=偉大なリポジトリ名は短い。思い出に残り、そして<strong>一意</strong>だ。
visibility=ビジビリティ
visiblity_helper=このリポジトリは<span class="ui red text">プライベート</span>です。
visiblity_fork_helper=(Change of this value will affect all forks)
visiblity_fork_helper=(この値の変更はすべてのフォークに適用されます)
fork_repo=フォークのリポジトリ
fork_from=フォーク元
fork_visiblity_helper=フォークされたリポジトリは可視状態を変更できません
repo_desc=説明
repo_lang=言語
repo_lang_helper=Select .gitignore files
repo_lang_helper=.gitignoreファイルを選択
license=ライセンス
license_helper=ライセンス ファイルを選択
readme=Readme
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
auto_init=Initialize this repository with selected files and template
create_repo=リポジトリを作成
default_branch=デフォルトのブランチ
mirror_interval=ミラー 間隔(時)
@@ -370,7 +385,7 @@ branch_and_tags=ブランチ& タグ
branches=ブランチ
tags=タグ
issues=課題
pulls=Pull Requests
pulls=プルリクエスト
labels=ラベル
milestones=マイルストーン
commits=コミット
@@ -418,9 +433,9 @@ issues.filter_type.all_issues=すべての問題
issues.filter_type.assigned_to_you=あなたに割り当てられました。
issues.filter_type.created_by_you=あなたが作成しました。
issues.filter_type.mentioning_you=あなたに伝える
issues.filter_sort=Sort
issues.filter_sort.latest=Newest
issues.filter_sort.oldest=Oldest
issues.filter_sort=並べ替え
issues.filter_sort.latest=最新
issues.filter_sort.oldest=最も古い
issues.filter_sort.recentupdate=Recently updated
issues.filter_sort.leastupdate=Least recently updated
issues.filter_sort.mostcomment=Most commented
@@ -630,7 +645,6 @@ release.tag_name_already_exist=このタグ名には既にリリースが存在
[org]
org_name_holder=組織名
org_name_helper=偉大な組織の名は短く覚えやすいです。
org_email_helper=組織の電子メールはすべての通知や確認を受け取ります。
create_org=組織を作成
repo_updated=更新した
people=人々
@@ -655,9 +669,9 @@ settings.full_name=フルネーム
settings.website=WEBサイト
settings.location=ロケーション
settings.update_settings=設定の更新
settings.change_orgname=組織名が変更されました
settings.change_orgname_desc=組織名が変更されています、継続しますか?これはすべての関連リンクに影響を与えます。
settings.update_setting_success=組織の設定が更新されました。
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=組織を削除
settings.delete_account=この組織を削除
settings.delete_prompt=操作はこの組織を完全に削除し、復旧<strong>できない</strong>
@@ -713,8 +727,9 @@ authentication=認証
config=コンフィギュレーション
notices=システム通知
monitor=モニタリング
prev=前へ
next=次へ
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=統計
dashboard.operations=操作
@@ -773,10 +788,13 @@ users.activated=アクティブ化
users.admin=アドミン
users.repos=リポジトリ
users.created=作成されました
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=編集
users.auth_source=認証元
users.auth_source=Authentication Source
users.local=ローカル
users.auth_login_name=認証ログイン名
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=アカウントのプロファイルが更新されました。
users.edit_account=アカウントの編集
users.is_activated=アカウントがアクティブされました
@@ -786,6 +804,7 @@ users.update_profile=アカウント ・ プロファイルを更新
users.delete_account=このアカウントを削除
users.still_own_repo=アカウント所有のリポジトリがあり、リポジトリの削除または所有者の移譲が必要です。
users.still_has_org=アカウントはまだ組織のメンバーであり、組織から退出するか削除する必要があります。
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=組織の管理パネル
orgs.name=名前
@@ -800,41 +819,47 @@ repos.watches=Watches
repos.stars=Stars
repos.issues=課題
auths.auth_manage_panel=承認の管理パネル
auths.new=新しい認証元を追加
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=名前
auths.type=タイプ
auths.enabled=Enabled
auths.updated=Updated
auths.auth_type=認証の種類
auths.auth_name=認証名
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=ドメイン
auths.host=ホスト
auths.port=ポート
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=名前属性
auths.attribute_surname=名字属性
auths.attribute_mail=Eメール属性
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=SMTP 認証の種類
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=SMTP ホスト
auths.smtpport=SMTP ポート
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=TLS 暗号化を有効にする
auths.skip_tls_verify=Skip TLS Verify
auths.pam_service_name=PAMサービス名
auths.enable_auto_register=自動登録を有効にする
auths.tips=ヒント
auths.edit=認証設定を編集
auths.edit=Edit Authentication Setting
auths.activated=認証がアクティブされました
auths.update_success=認証の設定が正常に更新されました。
auths.update=認証設定の更新
auths.delete=この権限を削除
auths.delete_auth_title=認証の削除
auths.delete_auth_desc=認証を削除します、継続しますか?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=サーバーの構成
config.app_name=アプリケーション名
@@ -858,14 +883,16 @@ config.db_user=ユーザ
config.db_ssl_mode=SSL モード
config.db_ssl_mode_helper=(「postgres」のみ
config.db_path=パス
config.db_path_helper=(sqlite3」のみ)
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=サービスの構成
config.register_email_confirm=電子メールの確認を必要
config.disable_register=登録を無効にする
config.show_registration_button=登録ボタンを表示します。
config.require_sign_in_view=サインインを要求
config.mail_notify=メール通知
config.enable_cache_avatar=アバターのキャッシュを有効にします。
config.mail_notify=メール通知
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=コードリンクの有効期限をアクティブ
config.reset_password_code_lives=パスワードリンクの有効期限をリセット
config.webhook_config=Webhook設定

View File

@@ -5,7 +5,6 @@ dashboard=Infopanelis
explore=Izpētīt
help=Palīdzība
sign_in=Pierakstīties
social_sign_in=Sociālā pieteikšanās: 2. solis <small>piesaistīt kontu</small>
sign_out=Izrakstīties
sign_up=Pieteikties
register=Reģistrēties
@@ -14,7 +13,7 @@ version=Versija
page=Lapa
template=Sagatave
language=Valoda
create_new=Izveidot jaunu...
create_new=Create...
user_profile_and_more=Lietotāja profilu un vairāk
signed_in_as=Pierakstījies kā
@@ -54,7 +53,8 @@ code=Kods
[install]
install=Instalācija
title=Instalācijas soļi pirmo reizi palaižot
requite_db_desc=Gogs ir nepieciešama MySQL, PostgreSQL vai SQLite3 datu bāze.
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=Datu bāzes iestatījumi
db_type=Datu bāzes veids
host=Resursdators
@@ -64,8 +64,11 @@ db_name=Datu bāzes nosaukums
db_helper=Nepieciešams izmantot MySQL INNODB dzini ar rakstzīmju kopu utf8_general_ci.
ssl_mode=SSL režīms
path=Ceļš
sqlite_helper=SQLite 3 datu bāzes faila atrašanās vieta.
err_empty_sqlite_path=Nav norādīts SQLite3 datu bāzes ceļš.
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=Gogs vispārīgie iestatījumi
app_name=Lietotnes nosaukums
@@ -99,6 +102,8 @@ disable_gravatar=Disable Gravatar Service
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration=Atspējot lietotāju reģistrāciju
disable_registration_popup=Atspējot lietotāju reģistrāciju, tikai administrators varēs izveidot jaunus lietotāju kontus.
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Iespējot nepieciešamību autorizēties, lai aplūkotu lapas
require_sign_in_view_popup=Tika autorizēti lietotāji var aplūkot lapas, neautorizēti lietotāji var piekļūt tikai autorizācijas un reģistrēšanās lapām.
admin_setting_desc=Nav nepieciešams izveidot administratora kontu uzreiz, lietotājs ar ID=1 saņems administratora tiesības automātiski.
@@ -143,7 +148,7 @@ forgot_password=Aizmirsu paroli
forget_password=Aizmirsi paroli?
sign_up_now=Nepieciešams konts? Reģistrējies tagad.
confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz <b>%s</b>, lūdzu, pārbaudies savu e-pasta kontu tuvāko %d stundu laikā, lai pabeigtu reģistrācijas procesu.
sign_in_email=Atvērt savu e-pasta kontu
sign_in_to_account=Sign in to your account
active_your_account=Aktivizēt savu kontu
resent_limit_prompt=Atvainojiet, Jūs sūtījāt aktivizācijas e-pastu pārāk bieži. Lūdzu, gaidiet 3 minūtes.
has_unconfirmed_mail=Sveiki %s, Jums ir neapstiprināta e-pasta adrese (<b>%s</b>). Ja neesat saņēmis apstiprināšanas e-pastu vai Jums ir nepieciešams nosūtīt jaunu, lūdzu, nospiediet pogu, kas atrodas zemāk.
@@ -155,6 +160,12 @@ invalid_code=Atvainojiet, Jūsu apstiprināšanas kodam ir beidzies derīguma te
reset_password_helper=Nospiediet šeit, lai atjaunotu paroli
password_too_short=Paroles garums nedrīkst būt mazāks par 6.
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=
no=
@@ -241,7 +252,7 @@ location=Atrašanās vieta
update_profile=Mainīt profilu
update_profile_success=Jūsu profila dati ir veiksmīgi saglabāti.
change_username=Lietotāja vārds mainīts
change_username_desc=Lietotājvārds tiks mainīts, vai vēlaties turpināt? Tas ietekmēs visas saites, kas attiecas uz Jūsu kontu.
change_username_prompt=This change will affect the way how links relate to your account.
continue=Turpināt
cancel=Atcelt
@@ -256,6 +267,7 @@ update_avatar_success=Jūsu profila bilde tika veiksmīgi saglabāta.
change_password=Mainīt paroli
old_password=Pašreizējā parole
new_password=Jauna parole
retype_new_password=Retype New Password
password_incorrect=Ievadīta nepareiza pašreizējā parole.
change_password_success=Parole tika veiksmīgi nomainīta. Tagad jūs varat pieraksītites, izmantojot jauno paroli.
@@ -265,9 +277,12 @@ email_desc=Primārā e-pasta adrese tiks izmantota sūtot notifikācijas un cit
primary=Primārā
primary_email=Iestatīt kā primāro
delete_email=Dzēst
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=Pievienot jaunu e-pasta adresi
add_email=Pievienot e-pastu
add_email_confirmation_sent=A new confirmation e-mail has been sent to <b>%s</b>, please check your inbox within the next %d hours to complete the confirmation process.
add_email_confirmation_sent=A new confirmation e-mail has been sent to '%s', please check your inbox within the next %d hours to complete the confirmation process.
add_email_success=Jūsu jaunā e-pasta adrese tika veiksmīgi pievienota.
manage_ssh_keys=Pārvaldīt SSH atslēgas
@@ -330,7 +345,7 @@ license=Licence
license_helper=Izvēlieties licences failu
readme=Readme
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
auto_init=Initialize this repository with selected files and template
create_repo=Izveidot repozitoriju
default_branch=Noklusējuma atzars
mirror_interval=Spoguļošanas intervāls (stundās)
@@ -418,9 +433,9 @@ issues.filter_type.all_issues=All issues
issues.filter_type.assigned_to_you=Assigned to you
issues.filter_type.created_by_you=Created by you
issues.filter_type.mentioning_you=Mentioning you
issues.filter_sort=Sort
issues.filter_sort.latest=Newest
issues.filter_sort.oldest=Oldest
issues.filter_sort=Kārtot
issues.filter_sort.latest=Jaunākie
issues.filter_sort.oldest=Vecakie
issues.filter_sort.recentupdate=Recently updated
issues.filter_sort.leastupdate=Least recently updated
issues.filter_sort.mostcomment=Most commented
@@ -429,12 +444,12 @@ issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a>
issues.opened_by_fake=opened %[1]s by %[2]s
issues.previous=Previous
issues.next=Next
issues.open_title=Open
issues.closed_title=Closed
issues.num_comments=%d comments
issues.open_title=Atvērta
issues.closed_title=Slēgta
issues.num_comments=%d komentāri
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=There is no content yet.
issues.close_issue=Close
issues.close_issue=Aizvērt
issues.close_comment_issue=Close and comment
issues.reopen_issue=Reopen
issues.reopen_comment_issue=Reopen and comment
@@ -447,9 +462,9 @@ issues.admin=Admin
issues.owner=Owner
issues.sign_up_for_free=Sign up for free
issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a>
issues.edit=Edit
issues.cancel=Cancel
issues.save=Save
issues.edit=Labot
issues.cancel=Atcelt
issues.save=Saglabāt
issues.label_title=Label name
issues.label_color=Label color
issues.label_count=%d labels
@@ -490,18 +505,18 @@ milestones.close_tab=%d Closed
milestones.closed=Closed %s
milestones.no_due_date=No due date
milestones.open=Open
milestones.close=Close
milestones.close=Aizvērt
milestones.new_subheader=Create milestones to organize your issues.
milestones.create=Create Milestone
milestones.title=Title
milestones.desc=Description
milestones.title=Virsraksts
milestones.desc=Apraksts
milestones.due_date=Due Date (optional)
milestones.clear=Clear
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'.
milestones.create_success=Milestone '%s' has been created successfully!
milestones.edit=Edit Milestone
milestones.edit_subheader=Use better description for milestones so people won't be confused.
milestones.cancel=Cancel
milestones.cancel=Atcelt
milestones.modify=Modify Milestone
milestones.edit_success=Changes of milestone '%s' has been saved successfully!
milestones.deletion=Milestone Deletion
@@ -585,8 +600,8 @@ settings.slack_channel=Kanāls
settings.deploy_keys=Izvietot atslēgas
settings.add_deploy_key=Add Deploy Key
settings.no_deploy_keys=You haven't added any deploy key.
settings.title=Title
settings.deploy_key_content=Content
settings.title=Virsraksts
settings.deploy_key_content=Saturs
settings.key_been_used=Deploy key content has been used.
settings.key_name_used=Deploy key with same name has already existed.
settings.add_key_success=New deploy key '%s' has been added successfully!
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Laidiens ar šādu taga nosaukumu jau eksistē.
[org]
org_name_holder=Organizācijas nosaukums
org_name_helper=Labi organizāciju nosaukumi ir īsi un tādi, kurus viegli atcerēties.
org_email_helper=Uz organizācijas e-pastu tiks sūtītas visas notifikācias un apstiprinājumi.
create_org=Izveidot organizāciju
repo_updated=Atjaunināts
people=Personas
@@ -655,9 +669,9 @@ settings.full_name=Pilns vārds, uzvārds
settings.website=Mājas lapa
settings.location=Atrašanās vieta
settings.update_settings=Mainīt iestatījumus
settings.change_orgname=Mainīts organizācijas nosaukums
settings.change_orgname_desc=Organizācijas nosaukums tiks mainīts, vai vēlaties turpinat? Tas ietekmēs saites, kas attiecas uz šo organizāciju.
settings.update_setting_success=Organizācijas iestatījumi tika veiksmīgi saglabāti.
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=Dzēst organizāciju
settings.delete_account=Dzēst šo organizāciju
settings.delete_prompt=Šī darbība pilnībā dzēsīs šo organizāciju, kā arī tā ir <strong>NEATGRIEZENISKA</strong>!
@@ -713,8 +727,9 @@ authentication=Autentifikācijas
config=Konfigurācija
notices=Sistēmas paziņojumi
monitor=Uzraudzība
prev=Iepr.
next=Tālāk
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=Statistika
dashboard.operations=Darbības
@@ -773,10 +788,13 @@ users.activated=Aktivizēts
users.admin=Administrators
users.repos=Repozitoriji
users.created=Izveidots
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=Labot
users.auth_source=Autorizācijas avots
users.auth_source=Authentication Source
users.local=Iebūvētā
users.auth_login_name=Autorizāciju, pietiekšanās vārds
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=Konta profils tika veiksmīgi saglabāts.
users.edit_account=Labot kontu
users.is_activated=Konts ir aktivizēts
@@ -786,6 +804,7 @@ users.update_profile=Mainīt konta profilu
users.delete_account=Dzēst šo kontu
users.still_own_repo=Šis konts ir vismaz viena repozitorija īpašnieks, tos sākumā ir nepieciešams izdzēst vai nomainīt to īpašnieku.
users.still_has_org=Šis konts ir vismaz vienas organizācijas biedrs, sākumā nepieciešams pamest vai izdzēst šo organizāciju.
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=Organizāciju pārvaldības panelis
orgs.name=Nosaukums
@@ -800,41 +819,47 @@ repos.watches=Vērošana
repos.stars=Atzīmētās zvaigznītes
repos.issues=Problēmas
auths.auth_manage_panel=Autorizāciju pārvaldības panelis
auths.new=Pievienot jaunu autorizācijas veidu
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=Nosaukums
auths.type=Veids
auths.enabled=Iespējota
auths.updated=Atjaunināta
auths.auth_type=Autorizācijas veids
auths.auth_name=Autorizācijas nosaukums
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=Domēns
auths.host=Resursdators
auths.port=Ports
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=First name attribute
auths.attribute_surname=Surname attribute
auths.attribute_mail=E-mail attribute
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=MS Ad SA
auths.smtp_auth=SMTP autorizācijas veids
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=SMTP resursdators
auths.smtpport=SMTP ports
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=Iespējot TLS šifrēšanu
auths.skip_tls_verify=Skip TLS Verify
auths.pam_service_name=PAM Service Name
auths.enable_auto_register=Iespējot automātisko reģistrāciju
auths.tips=Padomi
auths.edit=Labot autorizācijas iestatījumus
auths.edit=Edit Authentication Setting
auths.activated=Autentifikācija ir aktivizēta
auths.update_success=Autorizācijas iestatījumi tika veiksmīgi saglabāti.
auths.update=Mainīt autorizācijas iestatījumus
auths.delete=Dzēst šo autorizāciju
auths.delete_auth_title=Autorizācijas dzēšana
auths.delete_auth_desc=Šī autorizācija tiks dzēsta, vai vēlaties turpināt?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=Servera konfigurācija
config.app_name=Lietotnes nosaukums
@@ -858,14 +883,16 @@ config.db_user=Lietotājs
config.db_ssl_mode=SSL režīms
config.db_ssl_mode_helper=(tikai PostgreSQL datu bāzei)
config.db_path=Ceļš
config.db_path_helper=(tikai Sqlite3 datu bāzei)
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=Pakalpojuma konfigurācija
config.register_email_confirm=Pieprasīt e-pasta apstiprināšanu
config.disable_register=Atspējot jaunu lietotāju reģistrāciju
config.show_registration_button=Rādīt reģistrēšanās pogu
config.require_sign_in_view=Nepieciešama autorizācija
config.mail_notify=Pasta paziņojumi
config.enable_cache_avatar=Glabāt profila attēlus kešatmiņā
config.mail_notify=Pasta paziņojumi
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=Aktīvā koda ilgums
config.reset_password_code_lives=Paroles atiestatīšanas koda ilgums
config.webhook_config=Tīkla āķu konfigurācija
@@ -953,6 +980,6 @@ raw_minutes=minūtes
[dropzone]
default_message=Drop files here or click to upload.
invalid_input_type=You can't upload files of this type.
file_too_big=File size({{filesize}} MB) exceeds maximum size({{maxFilesize}} MB).
remove_file=Remove file
file_too_big=Faila izmērs ({{filesize}} MB) pārsniedz maksimālo atļauto izmēru ({{maxFilesize}} MB).
remove_file=Noņemt failu

View File

@@ -5,7 +5,6 @@ dashboard=Dashboard
explore=Verkennen
help=Help
sign_in=Inloggen
social_sign_in=Social netwerk inlog: tweede stap <small>account koppelen</small>
sign_out=Afmelden
sign_up=Aanmelden
register=Registreer
@@ -14,7 +13,7 @@ version=Versie
page=Pagina
template=Sjabloon
language=Taal
create_new=Maak een nieuwe...
create_new=Create...
user_profile_and_more=Gebruikersprofiel en meer
signed_in_as=Aangemeld als
@@ -54,7 +53,8 @@ code=Code
[install]
install=Installatie
title=Installatiestappen voor de eerste keer opstarten
requite_db_desc=Om Gogs te gebruiken is MySQL, PostgreSQL of SQLite3 vereist (SQLite3 is beschikbaar in de officiële versie).
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=Database instellingen
db_type=Database-type
host=Host
@@ -64,8 +64,11 @@ db_name=Database naam
db_helper=Gebruik InnoDB engine met utf8_general_ci karakterset voor MySQL.
ssl_mode=SSL-modus
path=Pad
sqlite_helper=Het pad naar de SQLite3 database.
err_empty_sqlite_path=SQLite3 database pad mag niet leeg zijn.
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=Toepassing algemene instellingen
app_name=Applicatienaam
@@ -99,6 +102,8 @@ disable_gravatar=Gravatar Service uitschakelen
disable_gravatar_popup=Schakel Gravatar en andere bronnen uit, alle avatars worden door gebruikers geüpload of zijn standaard.
disable_registration=Schakel zelfregistratie uit
disable_registration_popup=Schakel zelfregistratie uit, alleen admins kunnen accounts maken.
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Schakel vereiste aanmelding om pagina's te zien in
require_sign_in_view_popup=Alleen ingelogde gebruikers kunnen pagina's bekijken, bezoekers kunnen alleen de login/registratie pagina's zien.
admin_setting_desc=U hoeft niet meteen een administratie account te maken, de gebruiker met ID=1 krijgt automatisch administratierechten.
@@ -143,7 +148,7 @@ forgot_password=Wachtwoord vergeten
forget_password=Wachtwoord vergeten?
sign_up_now=Een account nodig? Meld u nu aan.
confirmation_mail_sent_prompt=Een bevestigingsemail is gestuurd naar <b>%s</b>, Bevestig u aanvraag binnen %d uren om uw registratie te voltooien.
sign_in_email=Meld u aan met uw e-mailadres
sign_in_to_account=Sign in to your account
active_your_account=Activeer uw account
resent_limit_prompt=Sorry, u heeft te snel na elkaar een aanvraag gedaan voor een activatie mail. Wacht drie minuten voor uw volgende aanvraag.
has_unconfirmed_mail=Beste %s, u heeft een onbevestigd e-mailadres (<b>%s</b>). Als u nog geen bevestiging heeft ontvangen, of u een nieuwe aanvraag wilt doen, klik dan op de onderstaande knop.
@@ -155,6 +160,12 @@ invalid_code=Sorry, uw bevestigingscode is verlopen of niet meer geldig.
reset_password_helper=Klik hier om uw wachtwoord opnieuw in te stellen.
password_too_short=De lengte van uw wachtwoord moet minimaal zes karakters zijn.
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=Ja
no=Nee
@@ -241,7 +252,7 @@ location=Locatie
update_profile=Profile bijwerken
update_profile_success=Uw profiel is succesvol bijgewerkt.
change_username=Username veranderd
change_username_desc=Gebruikersnaam is gewijzigd. Wilt u doorgaan? Dit zal gevolgen hebben voor alle koppelingen die betrekking hebben op uw account.
change_username_prompt=This change will affect the way how links relate to your account.
continue=Doorgaan
cancel=Annuleren
@@ -256,6 +267,7 @@ update_avatar_success=Instellingen voor avatar succesvol bijgewerkt.
change_password=Verander wachtwoord
old_password=Huidige wachtwoord
new_password=Nieuw wachtwoord
retype_new_password=Retype New Password
password_incorrect=Huidig wachtwoord is niet correct.
change_password_success=Wachtwoord is succesvol gewijzigd. U kunt nu met uw nieuwe wachtwoord inloggen.
@@ -265,9 +277,12 @@ email_desc=Uw primaire e-mailadres zal worden gebruikt voor meldingen en andere
primary=Primair
primary_email=Instellen als primair
delete_email=Verwijder
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=Nieuw e-mailadres toevoegen
add_email=E-mailadres toevoegen
add_email_confirmation_sent=Een nieuwe bevestiging e-mail werd verstuurd naar <b>%s</b>, gelieve uw inbox in de komende %d uren te controleren om het bevestigingsproces te voltooien.
add_email_confirmation_sent=Een nieuwe bevestiging e-mail werd verstuurd naar '%s', gelieve uw inbox in de komende %d uren te controleren om het bevestigingsproces te voltooien.
add_email_success=Het e-mailadres was toegevoegd.
manage_ssh_keys=Beheer SSH sleutels
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Versie met deze naam bestaat al.
[org]
org_name_holder=Organisatienaam
org_name_helper=Een goede organisatienaam is kort en memorabel.
org_email_helper=Alle notificaties en bevestigingen worden gestuurd naar het e-mailadres van de organisatie.
create_org=Nieuwe organisatie aanmaken
repo_updated=Geupdate
people=Mensen
@@ -655,9 +669,9 @@ settings.full_name=Volledige naam
settings.website=Website
settings.location=Locatie
settings.update_settings=Instellingen bijwerken
settings.change_orgname=Organisatie naam veranderd
settings.change_orgname_desc=De naam van de organisatie is veranderd, wilt u doorgaan? Dit zal gevolgen hebben voor alle koppelingen die betrekking hebben op deze organisatie.
settings.update_setting_success=Organisatie instellingen zijn succesvol bijgewerkt.
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=Verwijder organisatie
settings.delete_account=Verwijder deze organisatie
settings.delete_prompt=Deze actie zal de origanisatie permanent verwijderen. U kunt dit <strong>NIET</strong> terug draaien!
@@ -713,8 +727,9 @@ authentication=Autenticaties
config=Configuratie
notices=Systeem aankondigingen
monitor=Bijhouden
prev=Vorige
next=Volgende
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=Statistieken
dashboard.operations=Bewerkingen
@@ -773,10 +788,13 @@ users.activated=Geactiveerd
users.admin=Admin
users.repos=Repos
users.created=Aangemaakt
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=Bewerken
users.auth_source=Autorisatiebron
users.auth_source=Authentication Source
users.local=Lokaal
users.auth_login_name=Autorisatie inlognaam
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=Profiel is succesvol bijgewerkt.
users.edit_account=Bewerk account
users.is_activated=Dit account is geactiveerd
@@ -786,6 +804,7 @@ users.update_profile=Account profiel bijwerken
users.delete_account=Dit account verwijderen
users.still_own_repo=Dit account is nog steeds eigendom van een repositorie. U moet deze repositorie eerst verwijderen of overdragen.
users.still_has_org=Deze account nog steeds lidmaatschap van organisatie, u hebt naar links of hen eerst verwijderen.
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=Organisaties beheren
orgs.name=Naam
@@ -800,41 +819,47 @@ repos.watches=Volgers
repos.stars=Sterren
repos.issues=Kwesties
auths.auth_manage_panel=Autorisatiebeheerpaneel
auths.new=Nieuwe autorisatiebron
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=Naam
auths.type=Type
auths.enabled=Ingeschakeld
auths.updated=Bijgewerkt
auths.auth_type=Autorisatietype
auths.auth_name=Autorisatienaam
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=Domein
auths.host=Host
auths.port=Poort
auths.bind_dn=Binden DN
auths.bind_password=Bind Password
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=Voornaam attribuut
auths.attribute_surname=Achternaam attribuut
auths.attribute_mail=E-mail attribuut
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=MS Ad SA
auths.smtp_auth=SMTP authenticatietype
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=SMTP host
auths.smtpport=SMTP poort
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=Activeer TLS-encryptie
auths.skip_tls_verify=Skip TLS Verify
auths.pam_service_name=PAM servicenaam
auths.enable_auto_register=Activeer automatische registratie
auths.tips=Tips
auths.edit=Bewerk autorisatie-instellingen
auths.edit=Edit Authentication Setting
auths.activated=Deze autorisatiemethode is geactiveerd
auths.update_success=Autorisatie-instellingen zijn succesvol bijgewerkt.
auths.update=Update autorisatie-instellingen
auths.delete=Verwijder deze autorisatie
auths.delete_auth_title=Verwijderings-autorisatie
auths.delete_auth_desc=Deze autorisatiemethode wordt verwijderd. Weet u zeker dat u wilt doorgaan?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=Serverconfiguratie
config.app_name=Applicatienaam
@@ -858,14 +883,16 @@ config.db_user=Gebruiker
config.db_ssl_mode=SSL modus
config.db_ssl_mode_helper=(alleen voor "postgres")
config.db_path=Pad
config.db_path_helper=(alleen voor "sqlite3")
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=Serviceconfiguratie
config.register_email_confirm=E-mailbevestiging registreren
config.disable_register=Registratie uitgeschakeld
config.show_registration_button=Registeren knop weergeven
config.require_sign_in_view=Inloggen vereist om te kunnen inzien
config.mail_notify=E-mailnotificaties
config.enable_cache_avatar=Avatar Cache inschakelen
config.mail_notify=E-mailnotificaties
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=Actieve Code leven
config.reset_password_code_lives=Reset wachtwoord Code leven
config.webhook_config=Webhook configuratie

View File

@@ -5,7 +5,6 @@ dashboard=Pulpit
explore=Odkrywaj
help=Pomoc
sign_in=Zaloguj się
social_sign_in=Rejestracja przy pomocy sieci społecznościowych: 2 krok <small>kojarzenie konta</small>
sign_out=Wyloguj
sign_up=Zarejestruj się
register=Zarejestruj się
@@ -14,7 +13,7 @@ version=Wersja
page=Strona
template=Szablon
language=Język
create_new=Utwórz nowy...
create_new=Create...
user_profile_and_more=Profil użytkownika i więcej
signed_in_as=Zalogowany jako
@@ -54,7 +53,8 @@ code=Kod
[install]
install=Instalacja
title=Kroki instalacyjne dla pierwszego uruchomienia
requite_db_desc=Gogs wymaga MySQL, PostgreSQL lub SQLite3.
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=Ustawienia bazy danych
db_type=Typ bazy danych
host=Host
@@ -64,8 +64,11 @@ db_name=Nazwa bazy danych
db_helper=Proszę użyć silnika INNODB z kodowaniem utf8_general_ci dla MySQL.
ssl_mode=Tryb SSL
path=Ścieżka
sqlite_helper=Ścieżka do bazy SQLite3.
err_empty_sqlite_path=Ścieżka do bazy danych SQLite3 nie może być pusta.
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=Ustawienia ogólne Gogs
app_name=Nazwa aplikacji
@@ -99,6 +102,8 @@ disable_gravatar=Wyłącz usługę Gravatar
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration=Wyłącz samodzielną rejestrację
disable_registration_popup=Wyłącz samodzielną rejestrację użytkownika, tylko administrator będzie mógł tworzyć konta.
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Włącz wymóg zalogowania do przeglądania stron
require_sign_in_view_popup=Tylko zalogowani użytkownicy będą mogli przeglądać strony, goście zobaczą tylko stronę logowania.
admin_setting_desc=Nie musisz tworzyć konta administratora teraz, użytkownik z ID = 1 zyska dostęp administratora automatycznie.
@@ -143,7 +148,7 @@ forgot_password=Zapomniałem hasła
forget_password=Zapomniałeś hasła?
sign_up_now=Potrzebujesz konta? Zarejestruj się teraz.
confirmation_mail_sent_prompt=Nowa wiadomość e-mail z potwierdzeniem została wysłana do <b>%s</b>, proszę sprawdzić swoją skrzynkę odbiorczą w ciągu najbliższych godzin %d aby dokończyć proces rejestracji.
sign_in_email=Zaloguj się na swój adres e-mail
sign_in_to_account=Sign in to your account
active_your_account=Aktywuj swoje konto
resent_limit_prompt=Niestety, zbyt często wysyłasz e-mail aktywacyjny. Proszę odczekać 3 minuty.
has_unconfirmed_mail=Witaj, %s, masz niepotwierdzony adres e-mail (<b>%s</b>). Jeśli nie otrzymałeś wiadomości e-mail z potwierdzeniem lub potrzebujesz wysłać nową, kliknij na poniższy przycisk.
@@ -155,6 +160,12 @@ invalid_code=Niestety, twój kod potwierdzający wygasł lub jest nieprawidłowy
reset_password_helper=Kliknij tutaj, aby zresetować hasło
password_too_short=Długość hasła nie może być mniejsza niż 6 znaków.
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=Tak
no=Nie
@@ -241,7 +252,7 @@ location=Lolalizacja
update_profile=Zaktualizuj profil
update_profile_success=Twój profil został pomyślnie zaktualizowany.
change_username=Zmieniono nazwę użytkownika
change_username_desc=Zmieniono nazwę użytkownika, czy chcesz kontynuować? To wpłynie na wszystkie linki odnoszą się do swojego konta.
change_username_prompt=This change will affect the way how links relate to your account.
continue=Konynuuj
cancel=Anuluj
@@ -256,6 +267,7 @@ update_avatar_success=Ustawienia awatarów zostały pomyślnie zaktualizowane.
change_password=Zmień hasło
old_password=Aktualne hasło
new_password=Nowe hasło
retype_new_password=Retype New Password
password_incorrect=Bieżące hasło nie jest prawidłowe.
change_password_success=Hasło zostało zmienione pomyślnie. Możesz teraz zalogować się za pomocą nowego hasła.
@@ -265,9 +277,12 @@ email_desc=Twój podstawowy adres e-mail będzie używany dla powiadomień i inn
primary=Podstawowy
primary_email=Ustaw jako podstawowy
delete_email=Usuń
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=Dodaj nowy e-mail
add_email=Dodaj e-mail
add_email_confirmation_sent=Nowa wiadomość e-mail z potwierdzeniem została wysłana do <b>%s</b>, proszę sprawdzić swoją skrzynkę odbiorczą w ciągu %d godzin, aby dokończyć proces potwierdzania.
add_email_confirmation_sent=Nowa wiadomość e-mail z potwierdzeniem została wysłana do '%s', proszę sprawdzić swoją skrzynkę odbiorczą w ciągu %d godzin, aby dokończyć proces potwierdzania.
add_email_success=Twój nowy e-mail został dodany pomyślnie.
manage_ssh_keys=Zarządzaj kluczami SSH
@@ -330,7 +345,7 @@ license=Licencja
license_helper=Wybierz plik licencji
readme=Readme
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
auto_init=Initialize this repository with selected files and template
create_repo=Utwórz repozytorium
default_branch=Domyślna gałąź
mirror_interval=Odświeżanie mirrorów (godziny)
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Wersja o tej nazwie tagu już istnieje.
[org]
org_name_holder=Nazwa organizacji
org_name_helper=Świetne nazwy organizacji są krótkie i łatwe do zapamiętania.
org_email_helper=Adres e-mail organizacji otrzymuje wszystkie powiadomienia i potwierdzenia.
create_org=Utwórz organizację
repo_updated=Zaktualizowano
people=Ludzie
@@ -655,9 +669,9 @@ settings.full_name=Imię i Nazwisko
settings.website=Strona
settings.location=Lolalizacja
settings.update_settings=Aktualizuj ustawienia
settings.change_orgname=Zmieniono nazwę organizacji
settings.change_orgname_desc=Zmieniono nazwę organizacji. Wpływa to na powiązanie odnośników z organizacją. Czy chcesz kontynuować?
settings.update_setting_success=Ustawienia organizacji zostały pomyślnie zaktualizowane.
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=Usuń Organizację
settings.delete_account=Usuń tą organizację
settings.delete_prompt=Organizacja zostanie trwale usunięta, a to <strong>NIE MOŻE</strong> być cofnięte!
@@ -713,8 +727,9 @@ authentication=Uwierzytelnienia
config=Konfiguracja
notices=Powiadomienia systemowe
monitor=Monitorowanie
prev=Wstecz
next=Następny
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=Statystyki
dashboard.operations=Operacje
@@ -773,10 +788,13 @@ users.activated=Aktywowany
users.admin=Admin
users.repos=Repozytoria
users.created=Utworzony
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=Edytuj
users.auth_source=Źródła autoryzacji
users.auth_source=Authentication Source
users.local=Lokalne
users.auth_login_name=Login Autoryzacyjny
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=Profil konta został pomyślnie zaktualizowany.
users.edit_account=Edytuj konto
users.is_activated=To konto jest aktywne
@@ -786,6 +804,7 @@ users.update_profile=Zaktualizuj profil konta
users.delete_account=Usuń to konto
users.still_own_repo=Twoje konto jest dalej właścicielem repozytorium, musisz je usunąć lub przekazać.
users.still_has_org=Twoje konto dalej posiada członkostwo w organizacji, musisz ją opuścić bądź usunąć.
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=Panel zarządzania organizacją
orgs.name=Nazwa
@@ -800,41 +819,47 @@ repos.watches=Obserwujących
repos.stars=Polubienia
repos.issues=Problemy
auths.auth_manage_panel=Zarzadzanie Autoryzacja
auths.new=Dodaj nowe źródło autoryzacji
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=Nazwa
auths.type=Typ
auths.enabled=Włączono
auths.updated=Zaktualizowano
auths.auth_type=Typ autoryzacji
auths.auth_name=Nazwa autoryzacji
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=Domena
auths.host=Host
auths.port=Port
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=Atrybut imienia
auths.attribute_surname=Atrybut nazwiska
auths.attribute_mail=Atrybut email
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Typ autoryzacji SMTP
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=Serwer SMTP
auths.smtpport=Port SMTP
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=Włącz szyfrowanie TLS
auths.skip_tls_verify=Pomiń weryfikację protokołu TLS
auths.pam_service_name=Nazwa usługi PAM
auths.enable_auto_register=Włącz automatyczną rejestrację
auths.tips=Wskazówki
auths.edit=Edytuj ustawienia autoryzacji
auths.edit=Edit Authentication Setting
auths.activated=To uwierzytelnienie zostało aktywowane
auths.update_success=Ustawienia uwierzytelnienia zostały zaktualizowane pomyślnie.
auths.update=Zaktualizuj ustawienia autoryzacji
auths.delete=Usuń tą autoryzację
auths.delete_auth_title=Usuwanie autoryzacji
auths.delete_auth_desc=To uwierzytelnienie zostanie usunięte, czy chcesz kontynuować?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=Konfiguracja serwera
config.app_name=Nazwa Aplikacji
@@ -858,14 +883,16 @@ config.db_user=Użytkownik
config.db_ssl_mode=Tryb SSL
config.db_ssl_mode_helper=(tylko dla "postgres")
config.db_path=Ścieżka
config.db_path_helper=(tylko dla "sqlite3")
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=Konfiguracja usługi
config.register_email_confirm=Wymagaj potwierdzenia e-mail
config.disable_register=Wyłącz rejestrację
config.show_registration_button=Pokazuj przycisk rejestracji
config.require_sign_in_view=Wymagaj bycia zalogowanym
config.mail_notify=Powiadomienia e-mail
config.enable_cache_avatar=Włącz cache awatarów
config.mail_notify=Powiadomienia e-mail
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=Ważność kodów aktywacyjnych
config.reset_password_code_lives=Czas życia kodu resetowania hasła
config.webhook_config=Konfiguracja skryptów internetowych

View File

@@ -5,7 +5,6 @@ dashboard=Painel de controle
explore=Explorar
help=Ajuda
sign_in=Entrar
social_sign_in=Entrada Social: 2ª etapa <small>associar uma conta</small>
sign_out=Sair
sign_up=Cadastrar
register=Registrar
@@ -54,7 +53,8 @@ code=Código
[install]
install=Instalação
title=Etapas de instalação para Primeira Execução
requite_db_desc=Gogs requer MySQL, PostgreSQL ou SQLite3.
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requer MySQL, PostgreSQL, SQLite3 ou TiDB.
db_title=Configurações de Banco de Dados
db_type=Tipo do Banco de Dados
host=Host
@@ -64,8 +64,11 @@ db_name=Nome do Banco de Dados
db_helper=Por favor, use o mecanismo INNODB com o conjunto de caracteres utf8_general_ci para MySQL.
ssl_mode=Modo SSL
path=Caminho
sqlite_helper=O caminho do arquivo do banco de dados do SQLite3.
err_empty_sqlite_path=O caminho do arquivo de banco de dados SQLite3 não pode estar em branco.
sqlite_helper=O caminho do arquivo do banco de dados SQLite3 ou TiDB.
err_empty_db_path=O Caminho do banco de dados SQLite3 ou TiDB não pode ser vazio.
err_invalid_tidb_name=Nome do banco de dados TiDB não permite os caracteres "." e "-".
no_admin_and_disable_registration=Você não pode desabilitar o registro sem criar uma conta de administrador.
err_empty_admin_password=A senha de administrador não pode ser vazia.
general_title=Configurações Gerais do Gogs
app_name=Nome do Aplicativo
@@ -96,9 +99,11 @@ server_service_title=Configurações de Servidor e Outros Serviços
offline_mode=Ativar Modo Offline
offline_mode_popup=Desative o CDN mesmo em modo de produção, todos os recursos serão disponibilizados localmente.
disable_gravatar=Desativar Serviço Gravatar
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_gravatar_popup=Desabilitar o Gravatar e fontes personalizadas, todos os avatares são enviados por usuários ou padrão.
disable_registration=Desativar auto-registro
disable_registration_popup=Desativar o auto-registro de usuário, para que somente o administrador possa criar contas.
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Requerer autenticação para a visualização de páginas
require_sign_in_view_popup=Somente usuários autenticados podem ver todas as páginas, visitantes somente podem entrar ou se cadastrar.
admin_setting_desc=Você não precisa criar uma conta de administrador agora, no entanto o primeiro usuário (ID=1) automaticamente terá acesso de administrador.
@@ -125,9 +130,9 @@ my_repos=Meus Repositórios
collaborative_repos=Repositórios Colaborativos
my_orgs=Minhas Organizações
my_mirrors=Meus Espelhos
view_home=View %s
view_home=Ver %s
issues.in_your_repos=In your repositories
issues.in_your_repos=Em seus repositórios
[explore]
repos=Repositórios
@@ -143,7 +148,7 @@ forgot_password=Esqueci a Senha
forget_password=Esqueceu a senha?
sign_up_now=Precisa de uma conta? Cadastre-se agora.
confirmation_mail_sent_prompt=Um novo e-mail de confirmação foi enviado para <b>%s</b>, por favor, verifique sua caixa de entrada nas próximas %d horas para completar seu registro.
sign_in_email=Entre com seu e-mail
sign_in_to_account=Sign in to your account
active_your_account=Ativar Sua Conta
resent_limit_prompt=Desculpe, você está enviando um e-mail de ativação com muita frequência. Por favor, aguarde 3 minutos.
has_unconfirmed_mail=Oi %s, você possui um endereço de e-mail não confirmado (<b>%s</b>). Se você não recebeu um e-mail de confirmação ou precisa reenviar um novo, clique no botão abaixo.
@@ -155,6 +160,12 @@ invalid_code=Desculpe, seu código de confirmação expirou ou não é válido.
reset_password_helper=Clique aqui para redefinir sua senha
password_too_short=O comprimento da senha não pode ser menor que 6.
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=Sim
no=Não
@@ -241,7 +252,7 @@ location=Localização
update_profile=Atualizar o Perfil
update_profile_success=O seu perfil foi atualizado com sucesso.
change_username=Nome de Usuário Alterado
change_username_desc=O nome de usuário foi alterado, você quer continuar? Isto afetará todos os links relacionados à sua conta.
change_username_prompt=Essa alteração afetará o modo como ligações referem-se à sua conta.
continue=Continuar
cancel=Cancelar
@@ -256,6 +267,7 @@ update_avatar_success=Sua configuração de avatar foi atualizada com sucesso.
change_password=Mudança de senha
old_password=Senha Atual
new_password=Nova Senha
retype_new_password=Digite novamente a nova senha
password_incorrect=A senha atual não está correta.
change_password_success=A senha está alterada com sucesso. Você pode agora entrar com a senha nova.
@@ -265,9 +277,12 @@ email_desc=Seu endereço de email principal será usado para notificações e ou
primary=Principal
primary_email=Definir como principal
delete_email=Deletar
email_deletion=Exclusão do email
email_deletion_desc=Ao Excluir este endereço de e-mail será removido informações relacionadas com a sua conta. Você deseja continuar?
email_deletion_success=O E-mail foi excluído com sucesso!
add_new_email=Adicionar novo endereço de e-mail
add_email=Adicionar e-mail
add_email_confirmation_sent=Um novo e-mail de confirmação foi enviado para <b>%s</b>. Por favor, verifique sua Caixa de Entrada dentro das próximas %d horas, para concluir o processo de confirmação.
add_email_confirmation_sent=Um novo e-mail de confirmação foi enviado para '%s'. Por favor, verifique sua Caixa de Entrada dentro das próximas %d horas, para concluir o processo de confirmação.
add_email_success=Seu novo endereço de E-mail foi adicionado com sucesso.
manage_ssh_keys=Gerenciar Chaves SSH
@@ -281,14 +296,14 @@ key_name=Nome da Chave
key_content=Conteúdo
add_key_success=A nova chave pública '%s' foi adicionada com sucesso!
delete_key=Deletar
ssh_key_deletion=SSH Key Deletion
ssh_key_deletion_desc=Delete this SSH key will remove all related accesses for your account. Do you want to continue?
ssh_key_deletion_success=SSH key has been deleted successfully!
ssh_key_deletion=Exclusão da chave de SSH
ssh_key_deletion_desc=Ao Excluir esta chave de SSH será removido todos os acessos para sua conta. Você deseja continuar?
ssh_key_deletion_success=A chave de SSH foi excluída com sucesso!
add_on=Adicionado em
last_used=Última vez usado em
no_activity=Nenhuma atividade recente
key_state_desc=Usada a pelo menos 7 dias
token_state_desc=This token is used in last 7 days
token_state_desc=Este token é usado em pelo menos 7 dias
manage_social=Gerenciar Contas Sociais Associadas
social_desc=Esta é uma lista de contas sociais. Remova qualquer ligação que você não reconheça.
@@ -297,15 +312,15 @@ unbind_success=A conta social foi desvinculada.
manage_access_token=Gerenciar Tokens de Acesso Pessoal
generate_new_token=Gerar novo Token
tokens_desc=Tokens you have generated that can be used to access the Gogs APIs.
tokens_desc=Tokens gerados por você que podem ser usados para acessar a API do Gogs.
new_token_desc=Por enquanto, todo token terá acesso completo à sua conta.
token_name=Nome do Token
generate_token=Gerar Token
generate_token_succees=Novo token de acesso gerado com sucesso! Certifique-se de copiar seu novo token de acesso pessoal agora. Você não poderá vê-lo novamente!
delete_token=Excluir
access_token_deletion=Personal Access Token Deletion
access_token_deletion_desc=Delete this personal access token will remove all related accesses of application. Do you want to continue?
delete_token_success=Personal access token has been removed successfully! Don't forget to update your application as well.
access_token_deletion=Exclusão do Token de acesso pessoal
access_token_deletion_desc=Ao Excluir este token de acesso pessoal será removido todos os acessos do aplicativo. Você deseja continuar?
delete_token_success=O Token de acesso pessoal foi removido com sucesso! Não se esqueça de atualizar seus aplicativos também.
delete_account=Deletar Sua Conta
delete_prompt=A operação deletará sua conta permanentemente, e <strong>NÃO PODERÁ</strong> ser desfeita!
@@ -319,7 +334,7 @@ repo_name=Nome do Repositório
repo_name_helper=Nomes de repositórios bons são pequenos, memorizáveis e <strong>únicos</strong>.
visibility=Visibilidade
visiblity_helper=Este é um repositório <span class="ui red text"> privado</span>
visiblity_fork_helper=(Change of this value will affect all forks)
visiblity_fork_helper=(A alteração desse valor irá afetar todos os forks)
fork_repo=Fork o Repositório
fork_from=Fork de
fork_visiblity_helper=Não é possível alterar a visibilidade de um repositório bifurcado
@@ -329,8 +344,8 @@ repo_lang_helper=Selecione arquivos .gitignore
license=Licença
license_helper=Selecione um arquivo de licença
readme=Leia-me
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
readme_helper=Selecione um modelo de leiame
auto_init=Inicializar este repositório com os arquivos selecionados e modelo
create_repo=Criar Repositório
default_branch=Ramo padrão
mirror_interval=Intervalo de Espelho (hora)
@@ -425,7 +440,7 @@ issues.filter_sort.recentupdate=Mais recentemente atualizados
issues.filter_sort.leastupdate=Menos recentemente atualizados
issues.filter_sort.mostcomment=Mais comentados
issues.filter_sort.leastcomment=Menos comentados
issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a>
issues.opened_by=%[1]s foi aberto por <a href="/%[2]s">%[3]s</a>
issues.opened_by_fake=aberto %[1]s por %[2]s
issues.previous=Página anterior
issues.next=Próxima página
@@ -465,18 +480,18 @@ pulls.compare_changes=Comparar mudanças
pulls.compare_changes_desc=Comparar dois ramos e criar solicitação de pull com as mudanças.
pulls.compare_base=base
pulls.compare_compare=comparar
pulls.filter_branch=Filter branch
pulls.filter_branch=Filtrar branch
pulls.no_results=Nada encontrado.
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even.
pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
pulls.create=Create Pull Request
pulls.create=Criar Pull Request
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code>
pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
pulls.tab_conversation=Conversação
pulls.tab_commits=Commits
pulls.tab_files=Files changed
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
pulls.merged=Merged
pulls.tab_files=Arquivos alterados
pulls.reopen_to_merge=Por favor reabra esse pull request para executar a operação de merge.
pulls.merged=Merge realizado
pulls.has_merged=This pull request has been merged successfully!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
@@ -525,9 +540,9 @@ settings.delete=Deletar Este Repositório
settings.delete_desc=Uma vez que você deleta um repositório, não tem volta. Por favor, tenha certeza.
settings.transfer_notices_1=- You will lose access if new owner is a individual user.
settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners.
settings.transfer_form_title=Please enter following information to confirm your operation:
settings.delete_notices_1=- This operation <strong>CANNOT</strong> be undone.
settings.delete_notices_2=- This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators.
settings.transfer_form_title=Informe a seguinte informação para confirmar a sua operação:
settings.delete_notices_1=-Esta operação <strong>NÃO PODERÁ</strong> ser desfeita.
settings.delete_notices_2=- Esta operação irá apagar permanentemente o tudo deste repositório, incluindo os dados do Git, problemas, comentários e acessos dos colaboradores.
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion.
settings.delete_notices_fork_2=- If this repository is private, all forks will be removed at the same time.
settings.delete_notices_fork_3=- If you want to keep all forks after deletion, please change visibility of this repository to public first.
@@ -545,7 +560,7 @@ settings.hooks_desc=Hooks da web ou Webhooks permitem serviços externos serem n
settings.webhook_deletion=Delete Webhook
settings.webhook_deletion_desc=Delete this webhook will remove its information and all delivery history. Do you want to continue?
settings.webhook_deletion_success=Webhook has been deleted successfully!
settings.webhook.request=Request
settings.webhook.request=Solicitação
settings.webhook.response=Resposta
settings.webhook.headers=Cabeçalhos
settings.webhook.payload=Payload
@@ -559,17 +574,17 @@ settings.add_webhook_desc=Enviaremos uma solicitação <code>POST</code> para o
settings.payload_url=URL de carga
settings.content_type=Tipo de Conteúdo
settings.secret=Secreto
settings.slack_username=Username
settings.slack_icon_url=Icon URL
settings.slack_username=Usuário
settings.slack_icon_url=URL do ícone
settings.slack_color=Cor
settings.event_desc=Quais eventos você gostaria de acionar a esse hook da web?
settings.event_push_only=Apenas o evento <code>push</code>.
settings.event_send_everything=I need <strong>everything</strong>.
settings.event_choose=Let me choose what I need.
settings.event_send_everything=Preciso de <strong>tudo</strong>.
settings.event_choose=Deixe-me escolher o que eu preciso.
settings.event_create=Criar
settings.event_create_desc=Branch, or tag created
settings.event_create_desc=Branch ou Tag criado
settings.event_push=Push
settings.event_push_desc=Git push to a repository
settings.event_push_desc=Git push para o repositório
settings.active=Ativar
settings.active_helper=Enviaremos detalhes do evento quando este hook for acionado.
settings.add_hook_success=Novos hooks de web foram adicionados.
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Já existiu versão com esse nome de tag.
[org]
org_name_holder=Nome da Organização
org_name_helper=Nomes de grandes organizações são curtos e memoráveis.
org_email_helper=O E-mail da organização receberá todas as notificações e as confirmações.
create_org=Criar Organização
repo_updated=Atualizado
people=Pessoas
@@ -655,9 +669,9 @@ settings.full_name=Nome Completo
settings.website=Site
settings.location=Localização
settings.update_settings=Atualizar Configurações
settings.change_orgname=Nome da Organização Alterado
settings.change_orgname_desc=O nome da organização foi alterado, você quer continuar? Isto afetará todos os links que se relacionam a esta organização.
settings.update_setting_success=Configuração da organização atualizada com sucesso.
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=Deletar Organização
settings.delete_account=Deletar Esta Organização
settings.delete_prompt=A operação deletará esta organização permanentemente, e <strong>NÃO PODERÁ</strong> ser desfeita!
@@ -713,8 +727,9 @@ authentication=Autenticações
config=Configuração
notices=Sistema de notificações
monitor=Monitoramento
prev=Anterior
next=Próximo
first_page=Primeira
last_page=Última
total=Total: %d
dashboard.statistic=Estatística
dashboard.operations=Operações
@@ -773,10 +788,13 @@ users.activated=Ativado
users.admin=Administrador
users.repos=Repos
users.created=Criado
users.send_register_notify=Send Registration Notification To User
users.new_success=Nova conta '%s' foi criada com sucesso.
users.edit=Editar
users.auth_source=Fonte de Autorização
users.auth_source=Fonte da autenticação
users.local=Local
users.auth_login_name=Nome de Autorização de Login
users.auth_login_name=Nome de login da autenticação
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=O perfil da conta foi atualizado com sucesso.
users.edit_account=Editar Conta
users.is_activated=Esta conta está ativada
@@ -786,6 +804,7 @@ users.update_profile=Atualizar Perfil da Conta
users.delete_account=Deletar Esta Conta
users.still_own_repo=Sua conta ainda é proprietária do repositório, você tem que excluir ou transferi-lo primeiro.
users.still_has_org=Sua conta ainda faz parte da organização, você deve sair ou excluí-la primeiro.
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=Painel de Gerenciamento da Organização
orgs.name=Nome
@@ -800,41 +819,47 @@ repos.watches=Observadores
repos.stars=Estrelas
repos.issues=Problemas
auths.auth_manage_panel=Painel de Gerenciamento da Autorização
auths.new=Adicionar Nova Fonte de Autorização
auths.auth_manage_panel=Painel de gerenciamento da autenticação
auths.new=Adicionar nova fonte
auths.name=Nome
auths.type=Tipo
auths.enabled=Habilitado
auths.updated=Atualizado
auths.auth_type=Tipo da Autorização
auths.auth_name=Nome da Autorização
auths.auth_type=Tipo de autenticação
auths.auth_name=Nome da autenticação
auths.domain=Domínio
auths.host=Host
auths.port=Porta
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.user_base=User Search Base
auths.bind_password_helper=Atenção: Esta senha é armazenada em texto plano. Não use uma conta com muitos privilégios.
auths.user_base=Base de pesquisa do usuário
auths.user_dn=User DN
auths.attribute_name=Atributo primeiro nome
auths.attribute_surname=Atributo sobrenome
auths.attribute_mail=Atributo e-mail
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Tipo de Autorização de SMTP
auths.smtp_auth=Tipo de autenticação SMTP
auths.smtphost=Host SMTP
auths.smtpport=Porta SMTP
auths.allowed_domains=Domínios autorizados
auths.allowed_domains_helper=Deixe em branco para permitir qualquer domínio do host SMTP. Vários domínios devem ser separados por vírgula ','.
auths.enable_tls=Habilitar Criptografia TLS
auths.skip_tls_verify=Skip TLS Verify
auths.skip_tls_verify=Ignorar verificação de TLS
auths.pam_service_name=Nome de Serviço PAM
auths.enable_auto_register=Habilitar Registro Automático
auths.tips=Dicas
auths.edit=Editar Configuração da Autorização
auths.edit=Editar a configuração de autenticação
auths.activated=Esta autenticação foi ativada
auths.update_success=A configuração da autorização foi atualizada com sucesso.
auths.update=Atualizar Configuração da Autorização
auths.delete=Excluir Esta Autorização
auths.delete_auth_title=Exclusão da Autorização
auths.delete_auth_desc=Esta autorização será excluída, deseja continuar?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=A configuração da autenticação foi atualizada com sucesso.
auths.update=Atualizar a configuração da autenticação
auths.delete=Excluir esta autenticação
auths.delete_auth_title=Exclusão da autenticação
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=Configuração do Servidor
config.app_name=Nome do Aplicativo
@@ -858,14 +883,16 @@ config.db_user=Usuário
config.db_ssl_mode=Modo SSL
config.db_ssl_mode_helper=(apenas para "postgres")
config.db_path=Caminho
config.db_path_helper=(apenas para "sqlite3")
config.db_path_helper=(para "sqlite3" e "tidb")
config.service_config=Configuração do Serviço
config.register_email_confirm=Requerer Confirmação de E-mail
config.disable_register=Desabilitar Registro
config.show_registration_button=Mostrar Botão de Registo
config.require_sign_in_view=Requerer Entrar no Gogs para Ver
config.mail_notify=Notificação de Correio
config.enable_cache_avatar=Habilitar Cache de Avatar
config.mail_notify=Notificação de Correio
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Habilitar o Captcha
config.active_code_lives=Ativar Code Lives
config.reset_password_code_lives=Redefinir Senha de Code Lives
config.webhook_config=Configuração de Hook da Web

View File

@@ -5,7 +5,6 @@ dashboard=Панель мониторинга
explore=Обзор
help=Помощь
sign_in=Войти
social_sign_in=Вход через соцсеть: 2-й шаг <small>свяжите учетную запись</small>
sign_out=Выход
sign_up=Регистрация
register=Зарегистрироваться
@@ -14,7 +13,7 @@ version=Версия
page=Страница
template=Шаблон
language=Язык
create_new=Создать новый...
create_new=Create...
user_profile_and_more=Профиль и остальное
signed_in_as=Вы вошли как
@@ -54,7 +53,8 @@ code=Код
[install]
install=Установка
title=Установочные шаги для первого запуска
requite_db_desc=Для Gogs требуется MySQL, PostgreSQL или SQLite3.
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=Настройки базы данных
db_type=Тип базы данных
host=Хост
@@ -64,8 +64,11 @@ db_name=Имя базы данных
db_helper=Для MySQL используйте тип таблиц InnoDB с кодировкой utf8_general_ci.
ssl_mode=Режим SSL
path=Путь
sqlite_helper=Путь к файлу базы данных SQLite3.
err_empty_sqlite_path=Путь к базе данных SQLite3 не может быть пустым.
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=Общие параметры Gogs
app_name=Имя приложения
@@ -99,6 +102,8 @@ disable_gravatar=Отключить службу Gravatar
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration=Отключить самостоятельную регистрацию
disable_registration_popup=Запретить пользователям самостоятельную регистрацию, только администратор может создавать аккаунты.
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Разрешить требовать авторизацию для просмотра страниц
require_sign_in_view_popup=Только авторизированные пользователи могут просматривать страницы, посетители смогут увидеть только ссылку на авторизацию вверху страницы.
admin_setting_desc=Вы не должны создать учетную запись администратора прямо сейчас, пользователь с ID = 1 получит доступ с правами администратора автоматически.
@@ -143,7 +148,7 @@ forgot_password=Забыли пароль
forget_password=Забыли пароль?
sign_up_now=Нужен аккаунт? Зарегистрируйтесь.
confirmation_mail_sent_prompt=Новое письмо для подтверждения было направлено на <b>%s</b>, пожалуйста, проверьте ваш почтовый ящик в течение %d часов для завершения регистрации.
sign_in_email=Войдите в свой адрес электронной почты
sign_in_to_account=Sign in to your account
active_your_account=Активируйте свой аккаунт
resent_limit_prompt=Вы слишком часто отправляете письмо с активацией. Подождите 3 минуты, пожалуйста.
has_unconfirmed_mail=Здравствуйте, %s! У вас есть неподтвержденный адрес электронной почты (<b>%s</b>). Если вам не приходило письмо с подтверждением или нужно выслать новое письмо, нажмите на кнопку ниже.
@@ -155,6 +160,12 @@ invalid_code=Извините, ваш код подтверждения исте
reset_password_helper=Нажмите здесь, чтобы сбросить свой пароль
password_too_short=Длина пароля не менее 6 символов.
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=Да
no=Нет
@@ -241,7 +252,7 @@ location=Местоположение
update_profile=Обновить профиль
update_profile_success=Ваш профиль был успешно обновлен.
change_username=Имя пользователя изменено
change_username_desc=Имя пользователя изменено, вы хотите продолжить? Это повлияет на все ссылки, связанные с вашей учетной записью.
change_username_prompt=This change will affect the way how links relate to your account.
continue=Далее
cancel=Отмена
@@ -256,6 +267,7 @@ update_avatar_success=Настройка вашего аватара обнов
change_password=Сменить пароль
old_password=Текущий пароль
new_password=Новый пароль
retype_new_password=Retype New Password
password_incorrect=Текущий пароль не правильный.
change_password_success=Пароль сменен успешно. Теперь вы можете войти с новым паролем.
@@ -265,9 +277,12 @@ email_desc=Ваш основной адрес электронной почты
primary=Основной
primary_email=Установить как основной
delete_email=Удалить
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=Добавить новый адрес электронной почты
add_email=Добавить электронную почту
add_email_confirmation_sent=Новое подтверждение по электронной почте было отправлено<b>%s</b>, пожалуйста, проверьте свой почтовый ящик в течение следующих %d часов, чтобы завершить процесс подтверждения.
add_email_confirmation_sent=Новое подтверждение по электронной почте было отправлено '%s', пожалуйста, проверьте свой почтовый ящик в течение следующих %d часов, чтобы завершить процесс подтверждения.
add_email_success=Новый адрес электронной почты успешно добавлен.
manage_ssh_keys=Управление SSH ключами
@@ -325,12 +340,12 @@ fork_from=Ответвление от
fork_visiblity_helper=Ответвленному репозиторию нельзя поменять уровень видимости
repo_desc=Описание
repo_lang=Язык
repo_lang_helper=Select .gitignore files
repo_lang_helper=Выберите файлы .gitignore
license=Лицензия
license_helper=Выберите файл лицензии
readme=Readme
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
readme_helper=Выберите шаблон для файла readme
auto_init=Инициализировать этот репозиторий выбранными файлами и шаблоном
create_repo=Создать репозиторий
default_branch=Ветка по умолчанию
mirror_interval=Интервал зеркалирования (час)
@@ -390,9 +405,9 @@ commits.older=Раньше
commits.newer=Новее
issues.new=Новая задача
issues.new.labels=Labels
issues.new.no_label=No Label
issues.new.clear_labels=Clear labels
issues.new.labels=Метки
issues.new.no_label=Не метка
issues.new.clear_labels=Отчистить метки
issues.new.milestone=Milestone
issues.new.no_milestone=No Milestone
issues.new.clear_milestone=Clear milestone
@@ -418,9 +433,9 @@ issues.filter_type.all_issues=Все задачи
issues.filter_type.assigned_to_you=Назначено Вам
issues.filter_type.created_by_you=Созданные вами
issues.filter_type.mentioning_you=Вы упомянуты
issues.filter_sort=Sort
issues.filter_sort.latest=Newest
issues.filter_sort.oldest=Oldest
issues.filter_sort=Сортировать
issues.filter_sort.latest=Новейшие
issues.filter_sort.oldest=Старейшие
issues.filter_sort.recentupdate=Recently updated
issues.filter_sort.leastupdate=Least recently updated
issues.filter_sort.mostcomment=Most commented
@@ -630,7 +645,6 @@ release.tag_name_already_exist=Релиз с этим именем тега уж
[org]
org_name_holder=Название организации
org_name_helper=Лучшие названия организаций коротки и запоминаемы.
org_email_helper=Почта организации получает все уведомления и подтверждения.
create_org=Создать Организацию
repo_updated=Обновлено
people=Люди
@@ -655,9 +669,9 @@ settings.full_name=Полное имя
settings.website=Сайт
settings.location=Местоположение
settings.update_settings=Обновить настройки
settings.change_orgname=Имя Организации изменено
settings.change_orgname_desc=Изменилось название организации, вы хотите продолжить? Это повлияет на все ссылки относящиеся к этой Организации.
settings.update_setting_success=Настройки Организации были успешно обновлены.
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=Удалить Организацию
settings.delete_account=Удалить Эту Организацию
settings.delete_prompt=Это действие безвозвратно удалит эту организацию навсегда.
@@ -713,8 +727,9 @@ authentication=Авторизация
config=Настройки
notices=Системные уведомления
monitor=Мониторинг
prev=Предыдущий.
next=Следующий
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=Статистика
dashboard.operations=Операции
@@ -773,10 +788,13 @@ users.activated=Активирован
users.admin=Администратор
users.repos=Репозитории
users.created=Создано
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=Редактировать
users.auth_source=Источник авторизации
users.auth_source=Authentication Source
users.local=Локальный
users.auth_login_name=Authorization Login Name
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=Профиль учетной записи обновлен успешно.
users.edit_account=Изменение учетной записи
users.is_activated=Эта учетная запись активирована
@@ -786,6 +804,7 @@ users.update_profile=Обновить профиль учетной записи
users.delete_account=Удалить эту учетную запись
users.still_own_repo=На вашем аккаунте все еще остается как минимум один репозиторий, сначала вам нужно удалить или передать его.
users.still_has_org=This account still has membership in at least one organization, you have to leave or delete the organizations first.
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=Управление группами
orgs.name=Имя
@@ -800,41 +819,47 @@ repos.watches=Следят
repos.stars=В избранном
repos.issues=Вопросы
auths.auth_manage_panel=Authorization Manage Panel
auths.new=Add New Authorization Source
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=Имя
auths.type=Тип
auths.enabled=Включено
auths.updated=Обновлено
auths.auth_type=Тип авторизации
auths.auth_name=Название авторизации
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=Домен
auths.host=Хост
auths.port=Порт
auths.bind_dn=Bind DN
auths.bind_password=Bind Password
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=First name attribute
auths.attribute_surname=Surname attribute
auths.attribute_mail=E-mail attribute
auths.filter=User Filter
auths.admin_filter=Admin Filter
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Тип авторизации SMTP
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=Узел SMTP
auths.smtpport=SMTP-порт
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=Включение шифрования TLS
auths.skip_tls_verify=Skip TLS Verify
auths.pam_service_name=PAM Service Name
auths.enable_auto_register=Включить автоматическую регистрацию
auths.tips=Советы
auths.edit=Редактировать параметры авторизации
auths.edit=Edit Authentication Setting
auths.activated=Эта аутентификация активирована
auths.update_success=Настройка авторизации обновлена успешно.
auths.update=Обновить параметры авторизации
auths.delete=Удалить эту авторизацию
auths.delete_auth_title=Удаление авторизации
auths.delete_auth_desc=Эта авторизация будет удалена. Вы хотите продолжить?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=Конфигурация сервера
config.app_name=Имя приложения
@@ -858,14 +883,16 @@ config.db_user=Пользователь
config.db_ssl_mode=Режим SSL
config.db_ssl_mode_helper=(только для «postgres»)
config.db_path=Путь
config.db_path_helper=(for "sqlite3" only)
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=Service Configuration
config.register_email_confirm=Require E-mail Confirmation
config.disable_register=Отключить регистрацию
config.show_registration_button=Show Register Button
config.require_sign_in_view=Для просмотра необходима авторизация
config.mail_notify=Почтовые уведомления
config.enable_cache_avatar=Кешировать аватар
config.mail_notify=Почтовые уведомления
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=Active Code Lives
config.reset_password_code_lives=Reset Password Code Lives
config.webhook_config=Настройка автоматического обновления репозиции

View File

@@ -5,7 +5,6 @@ dashboard=控制面板
explore=探索
help=帮助
sign_in=登录
social_sign_in=社交帐号登录:第 2 步 <small>关联帐号</small>
sign_out=退出
sign_up=注册
register=注册
@@ -14,7 +13,7 @@ version=当前版本
page=页面
template=模板
language=语言选项
create_new=创建新的...
create_new=创建...
user_profile_and_more=用户信息及更多
signed_in_as=已登录用户
@@ -54,7 +53,8 @@ code=代码
[install]
install=安装页面
title=首次运行安装程序
requite_db_desc=Gogs 允许后端数据库为 MySQL、PostgreSQL 或 SQLite3
docker_helper=如果您正在使用 Docker 容器运行 Gogs请务必先仔细阅读 <a target="_blank" href="%s">官方文档</a> 后再对本页面进行填写
requite_db_desc=Gogs 要求安装 MySQL、PostgreSQL、SQLite3 或 TiDB。
db_title=数据库设置
db_type=数据库类型
host=数据库主机
@@ -64,8 +64,11 @@ db_name=数据库名称
db_helper=如果您使用 MySQL请使用 INNODB 引擎以及 utf8_general_ci 字符集。
ssl_mode=SSL 模式
path=数据库文件路径
sqlite_helper=SQLite3 数据库的文件路径。
err_empty_sqlite_path=SQLite 数据库文件路径不能为空。
sqlite_helper=SQLite3 或 TiDB 的数据库路径。
err_empty_db_path=SQLite3 或 TiDB 的数据库路径不能为空。
err_invalid_tidb_name=TiDB 数据库名称不允许包含字符 "." 或 "-" 。
no_admin_and_disable_registration=您不能够在未创建管理员用户的情况下禁止注册。
err_empty_admin_password=管理员密码不能为空。
general_title=应用基本设置
app_name=应用名称
@@ -99,6 +102,8 @@ disable_gravatar=禁用 Gravatar 服务
disable_gravatar_popup=禁用 Gravatar 和自定义源,仅使用由用户上传的或默认的头像。
disable_registration=禁止用户自主注册
disable_registration_popup=禁止用户自行注册功能,只有管理员可以添加帐号。
enable_captcha=启用验证码服务
enable_captcha_popup=要求在用户注册时输入预验证码
require_sign_in_view=启用登录访问限制
require_sign_in_view_popup=只有已登录的用户才能够访问页面,否则将只能看到登录或注册页面。
admin_setting_desc=创建管理员帐号并不是必须的,因为 ID=1 的用户将自动获得管理员权限。
@@ -143,7 +148,7 @@ forgot_password=忘记密码
forget_password=忘记密码?
sign_up_now=还没帐户?马上注册。
confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %d 小时内完成确认注册操作。
sign_in_email=登录到您的邮箱
sign_in_to_account=登录到您的帐户
active_your_account=激活您的帐户
resent_limit_prompt=对不起,您请求发送激活邮件过于频繁,请等待 3 分钟后再试!
has_unconfirmed_mail=%s 您好,系统检测到您有一封发送至 <b>%s</b> 但未被确认的邮件。如果您未收到激活邮件,或需要重新发送,请单击下方的按钮。
@@ -155,6 +160,12 @@ invalid_code=对不起,您的确认代码已过期或已失效。
reset_password_helper=单击此处重置密码
password_too_short=密码长度不能少于 6 位!
[mail]
activate_account=请激活您的帐户
activate_email=请验证您的邮箱地址
reset_password=重置您的密码
register_success=注册成功,欢迎使用
[modal]
yes=确认操作
no=取消操作
@@ -241,7 +252,7 @@ location=所在地区
update_profile=更新信息
update_profile_success=您的个人信息更新成功!
change_username=用户名将被修改
change_username_desc=用户名被修改,您确定要继续操作吗?这将会影响到所有与您帐户有关的链接
change_username_prompt=该操作将会影响到所有与您帐户有关的链接
continue=继续操作
cancel=取消操作
@@ -256,6 +267,7 @@ update_avatar_success=您的头像设置更新成功!
change_password=修改密码
old_password=当前密码
new_password=新的密码
retype_new_password=重新输入新的密码
password_incorrect=当前密码不正确!
change_password_success=密码修改成功!您现在可以使用新的密码登录。
@@ -265,9 +277,12 @@ email_desc=您的主要邮箱地址将被用于通知提醒和其它操作。
primary=主要
primary_email=设为主要
delete_email=删除
email_deletion=邮箱删除操作
email_deletion_desc=删除该邮箱地址将会移除所有相关的信息。是否继续?
email_deletion_success=邮箱删除成功!
add_new_email=添加新的邮箱地址
add_email=添加邮箱
add_email_confirmation_sent=一封待确认的电子邮件已发送到 <b>%s</b>,请在 %d 小时内检查您的收件箱,并完成确认过程。
add_email_confirmation_sent=一封待确认的电子邮件已发送到 '%s',请在 %d 小时内检查您的收件箱,并完成确认过程。
add_email_success=新的邮箱地址添加成功!
manage_ssh_keys=管理 SSH 密钥
@@ -281,7 +296,7 @@ key_name=密钥名称
key_content=密钥内容
add_key_success=新的 SSH 密钥 '%s' 添加成功!
delete_key=删除
ssh_key_deletion=删除 SSH 公钥
ssh_key_deletion=删除 SSH 公钥操作
ssh_key_deletion_desc=删除该 SSH 公钥将删除所有与您帐户相关的访问权限。是否继续?
ssh_key_deletion_success=SSH 公钥删除成功!
add_on=增加于
@@ -303,7 +318,7 @@ token_name=令牌名称
generate_token=生成令牌
generate_token_succees=新的操作令牌生成成功!您必须立即复制到一个安全的地方,因为该令牌只会显示一次!
delete_token=删除令牌
access_token_deletion=删除个人操作令牌
access_token_deletion=删除个人操作令牌操作
access_token_deletion_desc=删除该个人操作令牌将删除所有相关的应用程序的访问权限。是否继续?
delete_token_success=个人操作令牌删除成功!请更新与该令牌有关的所有应用。
@@ -457,7 +472,7 @@ issues.label_open_issues=%d 个开启的工单
issues.label_edit=编辑
issues.label_delete=删除
issues.label_modify=修改标签
issues.label_deletion=删除标签
issues.label_deletion=删除标签操作
issues.label_deletion_desc=删除该标签将会移除所有工单中相关的信息。是否继续?
issues.label_deletion_success=标签删除成功!
@@ -504,7 +519,7 @@ milestones.edit_subheader=使用更加清晰的描述来帮助人们更好地理
milestones.cancel=取消
milestones.modify=修改里程碑
milestones.edit_success=里程碑 '%s' 的修改内容已经生效!
milestones.deletion=删除里程碑
milestones.deletion=删除里程碑操作
milestones.deletion_desc=删除该里程碑将会移除所有工单中相关的信息。是否继续?
milestones.deletion_success=里程碑删除成功!
@@ -630,7 +645,6 @@ release.tag_name_already_exist=已经存在使用相同标签进行发布的版
[org]
org_name_holder=组织名称
org_name_helper=伟大的组织都有一个简短而寓意深刻的名字。
org_email_helper=组织的邮箱用于接收所有通知和确认邮件。
create_org=创建组织
repo_updated=最后更新于
people=组织成员
@@ -655,9 +669,9 @@ settings.full_name=组织全名
settings.website=官方网站
settings.location=所在地区
settings.update_settings=更新组织设置
settings.change_orgname=组织名称将被修改
settings.change_orgname_desc=组织名称被修改,您确定要继续操作吗?这将会影响到所有与该组织有关的链接。
settings.update_setting_success=组织设置更新成功!
settings.change_orgname_prompt=该操作将会影响到所有与该组织有关的链接
settings.update_avatar_success=组织头像更新成功!
settings.delete=删除组织
settings.delete_account=删除当前组织
settings.delete_prompt=删除操作会永久清除该组织的信息,并且 <strong>不可恢复</strong>
@@ -713,8 +727,9 @@ authentication=授权认证管理
config=应用配置管理
notices=系统提示管理
monitor=应用监控面板
prev=上一
next=下一
first_page=
last_page=
total=总计:%d
dashboard.statistic=应用统计数据
dashboard.operations=管理员操作
@@ -773,10 +788,13 @@ users.activated=已激活
users.admin=管理员
users.repos=仓库数
users.created=创建时间
users.send_register_notify=向用户发送注册通知邮件
users.new_success=新的用户 '%s' 创建成功!
users.edit=编辑
users.auth_source=认证源
users.local=本地
users.auth_login_name=认证登录名
users.auth_login_name=认证登录名
users.password_helper=将值留空使其保持不变。
users.update_profile_success=该用户信息更新成功!
users.edit_account=编辑用户信息
users.is_activated=该用户已被激活
@@ -786,6 +804,7 @@ users.update_profile=更新用户信息
users.delete_account=删除该用户
users.still_own_repo=该帐户仍然是某些仓库的拥有者,您必须先转移或删除它们才能执行删除帐户操作!
users.still_has_org=该帐户仍旧是某些组织的成员,您必须先使其离开或删除组织。
users.deletion_success=用户删除成功!
orgs.org_manage_panel=组织管理面板
orgs.name=组织名称
@@ -800,41 +819,47 @@ repos.watches=关注数
repos.stars=点赞数
repos.issues=工单数
auths.auth_manage_panel=授权认证管理面板
auths.new=添加新的认证
auths.auth_manage_panel=认证管理面板
auths.new=添加新的源
auths.name=认证名称
auths.type=认证类型
auths.enabled=已启用
auths.updated=最后更新时间
auths.auth_type=授权类型
auths.auth_name=授权名称
auths.auth_type=认证类型
auths.auth_name=认证名称
auths.domain=域名
auths.host=主机地址
auths.port=主机端口
auths.bind_dn=绑定 DN
auths.bind_password=绑定密码
auths.bind_password_helper=警告:该密码将会以明文的形式保存在数据库中。请不要使用拥有高权限的帐户!
auths.user_base=用户搜索基准
auths.user_dn=User DN
auths.attribute_name=名字属性
auths.attribute_surname=姓氏属性
auths.attribute_mail=邮箱属性
auths.filter=用户过滤规则
auths.admin_filter=管理员过滤规则
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=SMTP 授权类型
auths.smtp_auth=SMTP 认证类型
auths.smtphost=SMTP 主机地址
auths.smtpport=SMTP 主机端口
auths.allowed_domains=域名白名单
auths.allowed_domains_helper=将值留空表示不对域名做任何限制。多个域名之间需要使用逗号 ',' 分隔。
auths.enable_tls=启用 TLS 加密
auths.skip_tls_verify=忽略 TLS 验证
auths.pam_service_name=PAM 服务名称
auths.enable_auto_register=允许授权用户自动注册
auths.tips=帮助提示
auths.edit=修改授权认证设置
auths.edit=编辑认证设置
auths.activated=该授权认证已经启用
auths.update_success=授权认证设置更新成功!
auths.update=更新授权认证信息
auths.delete=删除该授权认证
auths.delete_auth_title=授权认证删除操作
auths.delete_auth_desc=该授权认证将被删除,您确定要继续吗?
auths.new_success=新的授权源 "%s" 添加成功!
auths.update_success=认证设置更新成功!
auths.update=更新认证设置
auths.delete=删除该认证
auths.delete_auth_title=删除认证操作
auths.delete_auth_desc=该认证将被删除。是否继续?
auths.deletion_success=授权源删除成功!
config.server_config=服务器配置
config.app_name=应用名称
@@ -858,14 +883,16 @@ config.db_user=连接用户
config.db_ssl_mode=SSL 模式
config.db_ssl_mode_helper=(仅限 "postgres" 使用)
config.db_path=数据库路径
config.db_path_helper=仅限 "sqlite3" 使用
config.db_path_helper=用于 "sqlite3" 和 "tidb"
config.service_config=服务配置
config.register_email_confirm=注册邮件确认
config.disable_register=关闭注册功能
config.show_registration_button=显示注册按钮
config.require_sign_in_view=强制登录浏览
config.mail_notify=邮件通知提醒
config.enable_cache_avatar=开启缓存头像
config.mail_notify=邮件通知提醒
config.disable_key_size_check=禁用密钥最小长度检查
config.enable_captcha=启用验证码服务
config.active_code_lives=激活用户链接有效期
config.reset_password_code_lives=重置密码链接有效期
config.webhook_config=Web 钩子配置

View File

@@ -5,7 +5,6 @@ dashboard=控制面版
explore=探索
help=幫助
sign_in=登錄
social_sign_in=社交帳號登錄:第 2 步 <small>關聯帳號</small>
sign_out=退出
sign_up=註冊
register=註冊
@@ -14,7 +13,7 @@ version=當前版本
page=頁面
template=模版
language=語言選項
create_new=創建新的...
create_new=Create...
user_profile_and_more=用戶信息及更多
signed_in_as=已登錄用戶
@@ -54,7 +53,8 @@ code=程式碼
[install]
install=安裝頁面
title=首次執行安裝程序
requite_db_desc=Gogs 允許後端數據庫為 MySQL、PostgreSQL 或 SQLite3但是 SQLite3 一般只有官方二進制發行版才支持。
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=數據庫設置
db_type=數據庫類型
host=數據庫主機
@@ -64,8 +64,11 @@ db_name=數據庫名稱
db_helper=如果您使用 MySQL請使用 INNODB 引擎以及 utf8_general_ci 字符集。
ssl_mode=SSL 模式
path=數據庫文件路徑
sqlite_helper=SQLite3 數據庫的文件路徑。
err_empty_sqlite_path=SQLite 數據庫文件路徑不能為空。
sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty.
general_title=應用基本設置
app_name=應用名稱
@@ -99,6 +102,8 @@ disable_gravatar=禁用 Gravatar 服務
disable_gravatar_popup=禁用 Gravatar 和自定義源,僅使用由用戶上傳或默認的頭像。
disable_registration=禁止用戶自主註冊
disable_registration_popup=禁止用戶自主註冊功能,只有管理員可以添加帳號。
enable_captcha=Enable Captcha
enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=啓用登錄訪問限制
require_sign_in_view_popup=只有已登錄的用戶才能夠訪問頁面,否則將只能看到登錄或註冊頁面。
admin_setting_desc=創建管理員帳號並不是必須的,因為 ID=1 的用戶將自動獲得管理員權限。
@@ -143,7 +148,7 @@ forgot_password=忘記密碼
forget_password=忘記密碼?
sign_up_now=還沒帳戶?馬上註冊。
confirmation_mail_sent_prompt=一封新的確認郵件已經被發送至 <b>%s</b>,請檢查您的收件箱並在 %d 小時內完成確認註冊操作。
sign_in_email=登錄到您的郵箱
sign_in_to_account=Sign in to your account
active_your_account=激活您的帳戶
resent_limit_prompt=對不起,您請求發送激活郵件過於頻繁,請等待 3 分鐘後再試!
has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到激活郵件,或需要重新發送,請單擊下方的按鈕。
@@ -155,6 +160,12 @@ invalid_code=對不起,您的確認代碼已過期或已失效。
reset_password_helper=單擊此處重置密碼
password_too_short=密碼長度不能少於 6 位!
[mail]
activate_account=Please activate your account
activate_email=Verify your e-mail address
reset_password=Reset your password
register_success=Register success, Welcome
[modal]
yes=確認操作
no=取消操作
@@ -241,7 +252,7 @@ location=所在地區
update_profile=更新信息
update_profile_success=您的個人信息更新成功!
change_username=用戶名將被修改
change_username_desc=用戶名被修改,您確定要繼續操作嗎?這將會影響到所有與您帳戶有關的連結。
change_username_prompt=This change will affect the way how links relate to your account.
continue=繼續操作
cancel=取消操作
@@ -256,6 +267,7 @@ update_avatar_success=您的頭像設置更新成功!
change_password=修改密碼
old_password=當前密碼
new_password=新的密碼
retype_new_password=Retype New Password
password_incorrect=當前密碼不正確!
change_password_success=密碼修改成功!您現在可以使用新的密碼登錄。
@@ -265,9 +277,12 @@ email_desc=您的主要邮箱地址将被用于通知提醒和其它操作。
primary=主要
primary_email=设为主要
delete_email=刪除
email_deletion=E-mail Deletion
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully!
add_new_email=添加新的電子郵件地址
add_email=添加電子郵件
add_email_confirmation_sent=一封待確認的電子郵件已發送到<b>%s</b>,請在%d 小時內檢查您的收件箱,並完成確認過程。
add_email_confirmation_sent=一封待確認的電子郵件已發送到 '%s',請在%d 小時內檢查您的收件箱,並完成確認過程。
add_email_success=新的邮箱地址添加成功。
manage_ssh_keys=管理 SSH 密鑰
@@ -330,7 +345,7 @@ license=授權許可
license_helper=請選擇授權許可文件
readme=Readme
readme_helper=Select a readme template
auto_init=Initialize this repository selected files and template
auto_init=Initialize this repository with selected files and template
create_repo=創建倉庫
default_branch=默認分支
mirror_interval=鏡像同步周期(小時)
@@ -630,7 +645,6 @@ release.tag_name_already_exist=已經存在使用相同標籤的發佈版本。
[org]
org_name_holder=組織名稱
org_name_helper=偉大的組織都有一個簡短而寓意深刻的名字。
org_email_helper=組織的郵箱用於接收所有通知和確認郵件。
create_org=創建組織
repo_updated=最後更新於
people=組織成員
@@ -655,9 +669,9 @@ settings.full_name=組織全名
settings.website=官方網站
settings.location=所在地區
settings.update_settings=更新組織設置
settings.change_orgname=組織名稱將被修改
settings.change_orgname_desc=組織名稱被修改,您確定要繼續操作嗎?這將會影響到所有與該組織有關的連結。
settings.update_setting_success=組織設置更新成功!
settings.change_orgname_prompt=This change will affect how links relate to the organization.
settings.update_avatar_success=Organization avatar setting has been updated successfully.
settings.delete=刪除組織
settings.delete_account=刪除當前組織
settings.delete_prompt=刪除操作會永久清除該組織的信息,並且 <strong>不可恢復</strong>
@@ -713,8 +727,9 @@ authentication=授權認證管理
config=應用配置管理
notices=系統提示管理
monitor=應用監控面版
prev=上一頁
next=下一頁
first_page=First
last_page=Last
total=Total: %d
dashboard.statistic=應用統計數據
dashboard.operations=管理員操作
@@ -773,10 +788,13 @@ users.activated=已激活
users.admin=管理員
users.repos=倉庫數
users.created=創建時間
users.send_register_notify=Send Registration Notification To User
users.new_success=New account '%s' has been created successfully.
users.edit=編輯
users.auth_source=認證源
users.auth_source=Authentication Source
users.local=本地
users.auth_login_name=認證登錄名
users.auth_login_name=Authentication Login Name
users.password_helper=Leave it empty to remain unchanged.
users.update_profile_success=該用戶信息更新成功!
users.edit_account=編輯用戶信息
users.is_activated=該用戶已被激活
@@ -786,6 +804,7 @@ users.update_profile=更新用戶信息
users.delete_account=刪除該用戶
users.still_own_repo=該帳戶仍然是某些倉庫的擁有者,您必須先轉移或刪除它們才能執行刪除帳戶操作!
users.still_has_org=該帳戶仍舊是某些組織的成員,您必須先使其離開或刪除組織。
users.deletion_success=Account has been deleted successfully!
orgs.org_manage_panel=組織管理面版
orgs.name=組織名稱
@@ -800,41 +819,47 @@ repos.watches=關註數
repos.stars=讚好數
repos.issues=問題數
auths.auth_manage_panel=授權認證管理面版
auths.new=添加新的認證源
auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source
auths.name=認證名稱
auths.type=認證類型
auths.enabled=已啟用
auths.updated=最後更新時間
auths.auth_type=授權類型
auths.auth_name=授權名稱
auths.auth_type=Authentication Type
auths.auth_name=Authentication Name
auths.domain=域名
auths.host=主機地址
auths.port=主機端口
auths.bind_dn=綁定DN
auths.bind_password=綁定密碼
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
auths.user_base=User Search Base
auths.user_dn=User DN
auths.attribute_name=名子屬性
auths.attribute_surname=姓氏屬性
auths.attribute_mail=電子郵箱屬性
auths.filter=使用者篩選器
auths.admin_filter=管理者篩選器
auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=SMTP 授權類型
auths.smtp_auth=SMTP Authentication Type
auths.smtphost=SMTP 主機地址
auths.smtpport=SMTP 主機端口
auths.allowed_domains=Allowed Domains
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=啟用 TLS 加密
auths.skip_tls_verify=Skip TLS Verify
auths.pam_service_name=PAM 服務名稱
auths.enable_auto_register=允許授權用戶自動註冊
auths.tips=幫助提示
auths.edit=修改授權認證設置
auths.edit=Edit Authentication Setting
auths.activated=該授權認證已經啟用
auths.update_success=授權認證設置更新成功!
auths.update=更新授權認證信息
auths.delete=刪除該授權認證
auths.delete_auth_title=授權認證刪除操作
auths.delete_auth_desc=該授權認證將被刪除,您確定要繼續嗎?
auths.new_success=New authentication '%s' has been added successfully.
auths.update_success=Authentication setting has been updated successfully.
auths.update=Update Authentication Setting
auths.delete=Delete This Authentication
auths.delete_auth_title=Authentication Deletion
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
auths.deletion_success=Authentication has been deleted successfully!
config.server_config=服務器配置
config.app_name=應用名稱
@@ -858,14 +883,16 @@ config.db_user=數據庫用戶
config.db_ssl_mode=SSL 模式
config.db_ssl_mode_helper=(僅限 "postgres" 使用)
config.db_path=數據庫路徑
config.db_path_helper=(僅限 "sqlite3" 使用)
config.db_path_helper=(for "sqlite3" and "tidb")
config.service_config=服務配置
config.register_email_confirm=註冊電子郵件確認
config.disable_register=關閉註冊功能
config.show_registration_button=顯示註冊按鈕
config.require_sign_in_view=強制登錄瀏覽
config.mail_notify=郵件通知提醒
config.enable_cache_avatar=開啟緩存頭像
config.mail_notify=郵件通知提醒
config.disable_key_size_check=Disable Minimum Key Size Check
config.enable_captcha=Enable Captcha
config.active_code_lives=激活用戶連結有效期
config.reset_password_code_lives=重置密碼連結有效期
config.webhook_config=Web 鉤子配置

View File

@@ -162,6 +162,17 @@
"outputPathIsSetByUser": 0,
"processed": 1
},
"\/public\/img\/gogs-large-resize.png": {
"fileType": 32768,
"ignore": 0,
"ignoreWasSetByUser": 0,
"initialSize": 54978,
"inputAbbreviatedPath": "\/public\/img\/gogs-large-resize.png",
"outputAbbreviatedPath": "\/public\/img\/gogs-large-resize.png",
"outputPathIsOutsideProject": 0,
"outputPathIsSetByUser": 0,
"processed": 0
},
"\/public\/img\/gogs-lg.png": {
"fileType": 32768,
"ignore": 0,
@@ -244,6 +255,46 @@
"strictMath": 0,
"strictUnits": 0
},
"\/public\/less\/_emojify.less": {
"allowInsecureImports": 0,
"createSourceMap": 0,
"disableJavascript": 0,
"fileType": 1,
"ieCompatibility": 1,
"ignore": 1,
"ignoreWasSetByUser": 0,
"inputAbbreviatedPath": "\/public\/less\/_emojify.less",
"outputAbbreviatedPath": "\/public\/css\/_emojify.css",
"outputPathIsOutsideProject": 0,
"outputPathIsSetByUser": 0,
"outputStyle": 0,
"relativeURLS": 0,
"shouldRunAutoprefixer": 0,
"shouldRunBless": 0,
"strictImports": 0,
"strictMath": 0,
"strictUnits": 0
},
"\/public\/less\/_explore.less": {
"allowInsecureImports": 0,
"createSourceMap": 0,
"disableJavascript": 0,
"fileType": 1,
"ieCompatibility": 1,
"ignore": 1,
"ignoreWasSetByUser": 0,
"inputAbbreviatedPath": "\/public\/less\/_explore.less",
"outputAbbreviatedPath": "\/public\/css\/_explore.css",
"outputPathIsOutsideProject": 0,
"outputPathIsSetByUser": 0,
"outputStyle": 0,
"relativeURLS": 0,
"shouldRunAutoprefixer": 0,
"shouldRunBless": 0,
"strictImports": 0,
"strictMath": 0,
"strictUnits": 0
},
"\/public\/less\/_form.less": {
"allowInsecureImports": 0,
"createSourceMap": 0,
@@ -1697,7 +1748,7 @@
"sassUseLibsass": 0,
"shouldRunAutoprefixer": 0,
"shouldRunBless": 0,
"skippedItemsString": "_cache, logs, \/public\/css, _logs, cache, \/public\/js\/lib, .git, \/public\/js, log, .svn, .hg",
"skippedItemsString": "\/public\/js, _logs, \/public\/js\/lib, .hg, _cache, log, logs, cache, .svn, .git, \/public\/img\/emoji, \/public\/css",
"slimAutoOutputPathEnabled": 1,
"slimAutoOutputPathFilenamePattern": "*.html",
"slimAutoOutputPathRelativePath": "",

View File

@@ -41,13 +41,24 @@ Most of settings are obvious and easy to understand, but there are some settings
- **Repository Root Path**: keep it as default value `/home/git/gogs-repositories` because `start.sh` already made a symbolic link for you.
- **Run User**: keep it as default value `git` because `start.sh` already setup a user with name `git`.
- **Domain**: fill in with Docker container IP(e.g. `192.168.99.100`).
- **Domain**: fill in with Docker container IP(e.g. `192.168.99.100`). But if you want to access your Gogs instance from a different physical machine, please fill in with the hostname or IP address of the Docker host machine.
- **SSH Port**: Use the exposed port from Docker container. For example, your SSH server listens on `22` inside Docker, but you expose it by `10022:22`, then use `10022` for this value.
- **HTTP Port**: Use port you want Gogs to listen on inside Docker container. For example, your Gogs listens on `3000` inside Docker, and you expose it by `10080:3000`, but you still use `3000` for this value.
- **Application URL**: Use combination of **Domain** and **exposed HTTP Port** values(e.g. `http://192.168.99.100:10080/`).
Full documentation of settings can be found [here](http://gogs.io/docs/advanced/configuration_cheat_sheet.html).
## Upgrade
:exclamation::exclamation::exclamation:<span style="color: red">**Make sure you have volumed data to somewhere outside Docker container**</span>:exclamation::exclamation::exclamation:
Steps to upgrade Gogs with Docker:
- `docker pull gogs/gogs`
- `docker stop gogs`
- `docker rm gogs`
- Finally, create container as the first time and don't forget to do same volume and port mapping.
## Troubleshooting
If you see the following error:

View File

@@ -21,10 +21,6 @@ fi
service ssh start
# sync templates
test -d /data/gogs/templates || cp -ar ./templates /data/gogs/
rsync -rtv /data/gogs/templates/ ./templates/
ln -sf /data/gogs/log ./log
ln -sf /data/gogs/data ./data
ln -sf /data/git /home/git

View File

@@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting"
)
const APP_VER = "0.6.9.0903 Beta"
const APP_VER = "0.6.15.0926 Beta"
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())

View File

@@ -189,7 +189,10 @@ func issueIndexTrimRight(c rune) bool {
// updateIssuesCommit checks if issues are manipulated by commit message.
func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*base.PushCommit) error {
for _, c := range commits {
// Commits are appended in the reverse order.
for i := len(commits) - 1; i >= 0; i-- {
c := commits[i]
refMarked := make(map[int64]bool)
for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
@@ -210,6 +213,9 @@ func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
@@ -220,7 +226,7 @@ func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string
url := fmt.Sprintf("%s/%s/%s/commit/%s", setting.AppSubUrl, repoUserName, repoName, c.Sha1)
message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message)
if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_COMMIT_REF, message, nil); err != nil {
if err = CreateRefComment(u, repo, issue, message, c.Sha1); err != nil {
return err
}
}
@@ -246,6 +252,9 @@ func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
@@ -283,6 +292,9 @@ func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
@@ -346,10 +358,14 @@ func CommitRepoAction(
}
if err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits); err != nil {
log.Debug("updateIssuesCommit: %v", err)
log.Error(4, "updateIssuesCommit: %v", err)
}
}
if len(commit.Commits) > setting.FeedMaxCommitNum {
commit.Commits = commit.Commits[:setting.FeedMaxCommitNum]
}
bs, err := json.Marshal(commit)
if err != nil {
return fmt.Errorf("Marshal: %v", err)

View File

@@ -50,11 +50,10 @@ func CountNotices() int64 {
return count
}
// GetNotices returns given number of notices with offset.
func GetNotices(num, offset int) ([]*Notice, error) {
notices := make([]*Notice, 0, num)
err := x.Limit(num, offset).Desc("id").Find(&notices)
return notices, err
// Notices returns number of notices in given page.
func Notices(page, pageSize int) ([]*Notice, error) {
notices := make([]*Notice, 0, pageSize)
return notices, x.Limit(pageSize, (page-1)*pageSize).Desc("id").Find(&notices)
}
// DeleteNotice deletes a system notice by given ID.

View File

@@ -15,7 +15,7 @@ import (
var c = cron.New()
func NewCronContext() {
func NewContext() {
var (
entry *cron.Entry
err error

View File

@@ -546,7 +546,7 @@ func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
// IssueUser represents an issue-user relation.
type IssueUser struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"uid INDEX"` // User ID.
UID int64 `xorm:"INDEX"` // User ID.
IssueID int64
RepoID int64 `xorm:"INDEX"`
MilestoneID int64
@@ -758,7 +758,7 @@ func GetUserIssueStats(repoID, uid int64, repoIDs []int64, filterMode int, isPul
queryStr := "SELECT COUNT(*) FROM `issue` "
baseCond := " WHERE issue.is_closed=?"
if repoID > 0 {
if repoID > 0 || len(repoIDs) == 0 {
baseCond += " AND issue.repo_id=" + com.ToStr(repoID)
} else {
baseCond += " AND issue.repo_id IN (" + strings.Join(base.Int64sToStrings(repoIDs), ",") + ")"
@@ -1619,7 +1619,7 @@ func DeleteMilestoneByID(mid int64) error {
return err
}
if _, err = sess.Id(m.ID).Delete(m); err != nil {
if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil {
return err
}
@@ -1685,6 +1685,9 @@ type Comment struct {
RenderedContent string `xorm:"-"`
Created time.Time `xorm:"CREATED"`
// Reference issue in commit message
CommitSHA string `xorm:"VARCHAR(40)"`
Attachments []*Attachment `xorm:"-"`
// For view issue page.
@@ -1733,14 +1736,15 @@ func (c *Comment) EventTag() string {
return "event-" + com.ToStr(c.ID)
}
func createComment(e *xorm.Session, u *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content string, uuids []string) (_ *Comment, err error) {
func createComment(e *xorm.Session, u *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content, commitSHA string, uuids []string) (_ *Comment, err error) {
comment := &Comment{
PosterID: u.Id,
Type: cmtType,
IssueID: issue.ID,
CommitID: commitID,
Line: line,
Content: content,
PosterID: u.Id,
Type: cmtType,
IssueID: issue.ID,
CommitID: commitID,
Line: line,
Content: content,
CommitSHA: commitSHA,
}
if _, err = e.Insert(comment); err != nil {
return nil, err
@@ -1819,18 +1823,18 @@ func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *I
if !issue.IsClosed {
cmtType = COMMENT_TYPE_REOPEN
}
return createComment(e, doer, repo, issue, 0, 0, cmtType, "", nil)
return createComment(e, doer, repo, issue, 0, 0, cmtType, "", "", nil)
}
// CreateComment creates comment of issue or commit.
func CreateComment(doer *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content string, attachments []string) (comment *Comment, err error) {
func CreateComment(doer *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content, commitSHA string, attachments []string) (comment *Comment, err error) {
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return nil, err
}
comment, err = createComment(sess, doer, repo, issue, commitID, line, cmtType, content, attachments)
comment, err = createComment(sess, doer, repo, issue, commitID, line, cmtType, content, commitSHA, attachments)
if err != nil {
return nil, err
}
@@ -1840,7 +1844,29 @@ func CreateComment(doer *User, repo *Repository, issue *Issue, commitID, line in
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
return CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMENT, content, attachments)
return CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMENT, content, "", attachments)
}
// CreateRefComment creates a commit reference comment to issue.
func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
if len(commitSHA) == 0 {
return fmt.Errorf("cannot create reference with empty commit SHA")
}
// Check if same reference from same commit has already existed.
has, err := x.Get(&Comment{
Type: COMMENT_TYPE_COMMIT_REF,
IssueID: issue.ID,
CommitSHA: commitSHA,
})
if err != nil {
return fmt.Errorf("check reference comment: %v", err)
} else if has {
return nil
}
_, err = CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMIT_REF, content, commitSHA, nil)
return err
}
// GetCommentByID returns the comment by given ID.

View File

@@ -13,6 +13,7 @@ import (
"strings"
"time"
"github.com/Unknwon/com"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
@@ -23,12 +24,14 @@ import (
type LoginType int
// Note: new type must be added at the end of list to maintain compatibility.
const (
NOTYPE LoginType = iota
PLAIN
LDAP
SMTP
PAM
DLDAP
)
var (
@@ -37,10 +40,11 @@ var (
ErrAuthenticationUserUsed = errors.New("Authentication has been used by some users")
)
var LoginTypes = map[LoginType]string{
LDAP: "LDAP",
SMTP: "SMTP",
PAM: "PAM",
var LoginNames = map[LoginType]string{
LDAP: "LDAP (via BindDN)",
DLDAP: "LDAP (simple auth)",
SMTP: "SMTP",
PAM: "PAM",
}
// Ensure structs implemented interface.
@@ -51,23 +55,24 @@ var (
)
type LDAPConfig struct {
ldap.Ldapsource
*ldap.Source
}
func (cfg *LDAPConfig) FromDB(bs []byte) error {
return json.Unmarshal(bs, &cfg.Ldapsource)
return json.Unmarshal(bs, &cfg)
}
func (cfg *LDAPConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg.Ldapsource)
return json.Marshal(cfg)
}
type SMTPConfig struct {
Auth string
Host string
Port int
TLS bool
SkipVerify bool
Auth string
Host string
Port int
AllowedDomains string `xorm:"TEXT"`
TLS bool
SkipVerify bool
}
func (cfg *SMTPConfig) FromDB(bs []byte) error {
@@ -91,32 +96,71 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) {
}
type LoginSource struct {
ID int64 `xorm:"pk autoincr"`
Type LoginType
Name string `xorm:"UNIQUE"`
IsActived bool `xorm:"NOT NULL DEFAULT false"`
Cfg core.Conversion `xorm:"TEXT"`
AllowAutoRegister bool `xorm:"NOT NULL DEFAULT false"`
Created time.Time `xorm:"CREATED"`
Updated time.Time `xorm:"UPDATED"`
ID int64 `xorm:"pk autoincr"`
Type LoginType
Name string `xorm:"UNIQUE"`
IsActived bool `xorm:"NOT NULL DEFAULT false"`
Cfg core.Conversion `xorm:"TEXT"`
Created time.Time `xorm:"CREATED"`
Updated time.Time `xorm:"UPDATED"`
}
func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
switch colName {
case "type":
switch LoginType((*val).(int64)) {
case LDAP:
case LDAP, DLDAP:
source.Cfg = new(LDAPConfig)
case SMTP:
source.Cfg = new(SMTPConfig)
case PAM:
source.Cfg = new(PAMConfig)
default:
panic("unrecognized login source type: " + com.ToStr(*val))
}
}
}
func (source *LoginSource) TypeString() string {
return LoginTypes[source.Type]
func (source *LoginSource) TypeName() string {
return LoginNames[source.Type]
}
func (source *LoginSource) IsLDAP() bool {
return source.Type == LDAP
}
func (source *LoginSource) IsDLDAP() bool {
return source.Type == DLDAP
}
func (source *LoginSource) IsSMTP() bool {
return source.Type == SMTP
}
func (source *LoginSource) IsPAM() bool {
return source.Type == PAM
}
func (source *LoginSource) UseTLS() bool {
switch source.Type {
case LDAP, DLDAP:
return source.LDAP().UseSSL
case SMTP:
return source.SMTP().TLS
}
return false
}
func (source *LoginSource) SkipVerify() bool {
switch source.Type {
case LDAP, DLDAP:
return source.LDAP().SkipVerify
case SMTP:
return source.SMTP().SkipVerify
}
return false
}
func (source *LoginSource) LDAP() *LDAPConfig {
@@ -131,12 +175,18 @@ func (source *LoginSource) PAM() *PAMConfig {
return source.Cfg.(*PAMConfig)
}
// CountLoginSources returns number of login sources.
func CountLoginSources() int64 {
count, _ := x.Count(new(LoginSource))
return count
}
func CreateSource(source *LoginSource) error {
_, err := x.Insert(source)
return err
}
func GetAuths() ([]*LoginSource, error) {
func LoginSources() ([]*LoginSource, error) {
auths := make([]*LoginSource, 0, 5)
return auths, x.Find(&auths)
}
@@ -157,107 +207,32 @@ func UpdateSource(source *LoginSource) error {
return err
}
func DelLoginSource(source *LoginSource) error {
cnt, err := x.Count(&User{LoginSource: source.ID})
func DeleteSource(source *LoginSource) error {
count, err := x.Count(&User{LoginSource: source.ID})
if err != nil {
return err
}
if cnt > 0 {
} else if count > 0 {
return ErrAuthenticationUserUsed
}
_, err = x.Id(source.ID).Delete(&LoginSource{})
_, err = x.Id(source.ID).Delete(new(LoginSource))
return err
}
// UserSignIn validates user name and password.
func UserSignIn(uname, passwd string) (*User, error) {
u := new(User)
if strings.Contains(uname, "@") {
u = &User{Email: uname}
} else {
u = &User{LowerName: strings.ToLower(uname)}
}
has, err := x.Get(u)
if err != nil {
return nil, err
}
if u.LoginType == NOTYPE && has {
u.LoginType = PLAIN
}
// For plain login, user must exist to reach this line.
// Now verify password.
if u.LoginType == PLAIN {
if !u.ValidatePassword(passwd) {
return nil, ErrUserNotExist{u.Id, u.Name}
}
return u, nil
}
if !has {
var sources []LoginSource
if err = x.UseBool().Find(&sources,
&LoginSource{IsActived: true, AllowAutoRegister: true}); err != nil {
return nil, err
}
for _, source := range sources {
if source.Type == LDAP {
u, err := LoginUserLdapSource(nil, uname, passwd,
source.ID, source.Cfg.(*LDAPConfig), true)
if err == nil {
return u, nil
}
log.Warn("Fail to login(%s) by LDAP(%s): %v", uname, source.Name, err)
} else if source.Type == SMTP {
u, err := LoginUserSMTPSource(nil, uname, passwd,
source.ID, source.Cfg.(*SMTPConfig), true)
if err == nil {
return u, nil
}
log.Warn("Fail to login(%s) by SMTP(%s): %v", uname, source.Name, err)
} else if source.Type == PAM {
u, err := LoginUserPAMSource(nil, uname, passwd,
source.ID, source.Cfg.(*PAMConfig), true)
if err == nil {
return u, nil
}
log.Warn("Fail to login(%s) by PAM(%s): %v", uname, source.Name, err)
}
}
return nil, ErrUserNotExist{u.Id, u.Name}
}
var source LoginSource
hasSource, err := x.Id(u.LoginSource).Get(&source)
if err != nil {
return nil, err
} else if !hasSource {
return nil, ErrLoginSourceNotExist
} else if !source.IsActived {
return nil, ErrLoginSourceNotActived
}
switch u.LoginType {
case LDAP:
return LoginUserLdapSource(u, u.LoginName, passwd, source.ID, source.Cfg.(*LDAPConfig), false)
case SMTP:
return LoginUserSMTPSource(u, u.LoginName, passwd, source.ID, source.Cfg.(*SMTPConfig), false)
case PAM:
return LoginUserPAMSource(u, u.LoginName, passwd, source.ID, source.Cfg.(*PAMConfig), false)
}
return nil, ErrUnsupportedLoginType
}
// .____ ________ _____ __________
// | | \______ \ / _ \\______ \
// | | | | \ / /_\ \| ___/
// | |___ | ` \/ | \ |
// |_______ \/_______ /\____|__ /____|
// \/ \/ \/
// Query if name/passwd can login against the LDAP directory pool
// Create a local user if success
// Return the same LoginUserPlain semantic
// FIXME: https://github.com/gogits/gogs/issues/672
func LoginUserLdapSource(u *User, name, passwd string, sourceId int64, cfg *LDAPConfig, autoRegister bool) (*User, error) {
fn, sn, mail, admin, logged := cfg.Ldapsource.SearchEntry(name, passwd)
func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, autoRegister bool) (*User, error) {
cfg := source.Cfg.(*LDAPConfig)
directBind := (source.Type == DLDAP)
fn, sn, mail, admin, logged := cfg.SearchEntry(name, passwd, directBind)
if !logged {
// User not in LDAP, do nothing
return nil, ErrUserNotExist{0, name}
@@ -275,11 +250,10 @@ func LoginUserLdapSource(u *User, name, passwd string, sourceId int64, cfg *LDAP
u = &User{
LowerName: strings.ToLower(name),
Name: name,
FullName: fn + " " + sn,
LoginType: LDAP,
LoginSource: sourceId,
FullName: strings.TrimSpace(fn + " " + sn),
LoginType: source.Type,
LoginSource: source.ID,
LoginName: name,
Passwd: passwd,
Email: mail,
IsAdmin: admin,
IsActive: true,
@@ -287,6 +261,13 @@ func LoginUserLdapSource(u *User, name, passwd string, sourceId int64, cfg *LDAP
return u, CreateUser(u)
}
// _________ __________________________
// / _____/ / \__ ___/\______ \
// \_____ \ / \ / \| | | ___/
// / \/ Y \ | | |
// /_______ /\____|__ /____| |____|
// \/ \/
type loginAuth struct {
username, password string
}
@@ -316,9 +297,7 @@ const (
SMTP_LOGIN = "LOGIN"
)
var (
SMTPAuths = []string{SMTP_PLAIN, SMTP_LOGIN}
)
var SMTPAuths = []string{SMTP_PLAIN, SMTP_LOGIN}
func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
@@ -357,6 +336,16 @@ func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
// Create a local user if success
// Return the same LoginUserPlain semantic
func LoginUserSMTPSource(u *User, name, passwd string, sourceId int64, cfg *SMTPConfig, autoRegister bool) (*User, error) {
// Verify allowed domains.
if len(cfg.AllowedDomains) > 0 {
idx := strings.Index(name, "@")
if idx == -1 {
return nil, ErrUserNotExist{0, name}
} else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), name[idx+1:]) {
return nil, ErrUserNotExist{0, name}
}
}
var auth smtp.Auth
if cfg.Auth == SMTP_PLAIN {
auth = smtp.PlainAuth("", name, passwd, cfg.Host)
@@ -368,7 +357,7 @@ func LoginUserSMTPSource(u *User, name, passwd string, sourceId int64, cfg *SMTP
if err := SMTPAuth(auth, cfg); err != nil {
if strings.Contains(err.Error(), "Username and Password not accepted") {
return nil, ErrUserNotExist{u.Id, u.Name}
return nil, ErrUserNotExist{0, name}
}
return nil, err
}
@@ -397,13 +386,20 @@ func LoginUserSMTPSource(u *User, name, passwd string, sourceId int64, cfg *SMTP
return u, err
}
// __________ _____ _____
// \______ \/ _ \ / \
// | ___/ /_\ \ / \ / \
// | | / | \/ Y \
// |____| \____|__ /\____|__ /
// \/ \/
// Query if name/passwd can login against PAM
// Create a local user if success
// Return the same LoginUserPlain semantic
func LoginUserPAMSource(u *User, name, passwd string, sourceId int64, cfg *PAMConfig, autoRegister bool) (*User, error) {
if err := pam.PAMAuth(cfg.ServiceName, name, passwd); err != nil {
if strings.Contains(err.Error(), "Authentication failure") {
return nil, ErrUserNotExist{u.Id, u.Name}
return nil, ErrUserNotExist{0, name}
}
return nil, err
}
@@ -426,3 +422,73 @@ func LoginUserPAMSource(u *User, name, passwd string, sourceId int64, cfg *PAMCo
err := CreateUser(u)
return u, err
}
func ExternalUserLogin(u *User, name, passwd string, source *LoginSource, autoRegister bool) (*User, error) {
if !source.IsActived {
return nil, ErrLoginSourceNotActived
}
switch source.Type {
case LDAP, DLDAP:
return LoginUserLDAPSource(u, name, passwd, source, autoRegister)
case SMTP:
return LoginUserSMTPSource(u, name, passwd, source.ID, source.Cfg.(*SMTPConfig), autoRegister)
case PAM:
return LoginUserPAMSource(u, name, passwd, source.ID, source.Cfg.(*PAMConfig), autoRegister)
}
return nil, ErrUnsupportedLoginType
}
// UserSignIn validates user name and password.
func UserSignIn(uname, passwd string) (*User, error) {
var u *User
if strings.Contains(uname, "@") {
u = &User{Email: uname}
} else {
u = &User{LowerName: strings.ToLower(uname)}
}
userExists, err := x.Get(u)
if err != nil {
return nil, err
}
if userExists {
switch u.LoginType {
case NOTYPE, PLAIN:
if u.ValidatePassword(passwd) {
return u, nil
}
return nil, ErrUserNotExist{u.Id, u.Name}
default:
var source LoginSource
hasSource, err := x.Id(u.LoginSource).Get(&source)
if err != nil {
return nil, err
} else if !hasSource {
return nil, ErrLoginSourceNotExist
}
return ExternalUserLogin(u, u.LoginName, passwd, &source, false)
}
}
var sources []LoginSource
if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil {
return nil, err
}
for _, source := range sources {
u, err := ExternalUserLogin(nil, uname, passwd, &source, true)
if err == nil {
return u, nil
}
log.Warn("Failed to login '%s' via '%s': %v", uname, source.Name, err)
}
return nil, ErrUserNotExist{u.Id, u.Name}
}

View File

@@ -46,7 +46,7 @@ func sessionRelease(sess *xorm.Session) {
// Note: get back time.Time from database Go sees it at UTC where they are really Local.
// So this function makes correct timezone offset.
func regulateTimeZone(t time.Time) time.Time {
if setting.UseSQLite3 {
if !setting.UseMySQL {
return t
}
@@ -54,12 +54,13 @@ func regulateTimeZone(t time.Time) time.Time {
if len(zone) != 5 {
return t
}
offset := com.StrTo(zone[2:3]).MustInt()
hour := com.StrTo(zone[2:3]).MustInt()
minutes := com.StrTo(zone[3:5]).MustInt()
if zone[0] == '-' {
return t.Add(time.Duration(offset) * time.Hour)
return t.Add(time.Duration(hour) * time.Hour).Add(time.Duration(minutes) * time.Minute)
}
return t.Add(-1 * time.Duration(offset) * time.Hour)
return t.Add(-1 * time.Duration(hour) * time.Hour).Add(-1 * time.Duration(minutes) * time.Minute)
}
var (
@@ -72,11 +73,12 @@ var (
}
EnableSQLite3 bool
EnableTidb bool
)
func init() {
tables = append(tables,
new(User), new(PublicKey), new(Oauth2), new(AccessToken),
new(User), new(PublicKey), new(AccessToken),
new(Repository), new(DeployKey), new(Collaboration), new(Access),
new(Watch), new(Star), new(Follow), new(Action),
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
@@ -86,13 +88,13 @@ func init() {
new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
new(Notice), new(EmailAddress))
gonicNames := []string{"SSL"}
gonicNames := []string{"UID", "SSL"}
for _, name := range gonicNames {
core.LintGonicMapper[name] = true
}
}
func LoadModelsConfig() {
func LoadConfigs() {
sec := setting.Cfg.Section("database")
DbCfg.Type = sec.Key("DB_TYPE").String()
switch DbCfg.Type {
@@ -102,6 +104,8 @@ func LoadModelsConfig() {
setting.UseMySQL = true
case "postgres":
setting.UsePostgreSQL = true
case "tidb":
setting.UseTiDB = true
}
DbCfg.Host = sec.Key("HOST").String()
DbCfg.Name = sec.Key("NAME").String()
@@ -143,6 +147,14 @@ func getEngine() (*xorm.Engine, error) {
return nil, fmt.Errorf("Fail to create directories: %v", err)
}
cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc"
case "tidb":
if !EnableTidb {
return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
}
if err := os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm); err != nil {
return nil, fmt.Errorf("Fail to create directories: %v", err)
}
cnnstr = "goleveldb://" + DbCfg.Path
default:
return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
}
@@ -224,11 +236,11 @@ func GetStatistic() (stats Statistic) {
stats.Counter.Access, _ = x.Count(new(Access))
stats.Counter.Issue, _ = x.Count(new(Issue))
stats.Counter.Comment, _ = x.Count(new(Comment))
stats.Counter.Oauth, _ = x.Count(new(Oauth2))
stats.Counter.Oauth = 0
stats.Counter.Follow, _ = x.Count(new(Follow))
stats.Counter.Mirror, _ = x.Count(new(Mirror))
stats.Counter.Release, _ = x.Count(new(Release))
stats.Counter.LoginSource, _ = x.Count(new(LoginSource))
stats.Counter.LoginSource = CountLoginSources()
stats.Counter.Webhook, _ = x.Count(new(Webhook))
stats.Counter.Milestone, _ = x.Count(new(Milestone))
stats.Counter.Label, _ = x.Count(new(Label))

18
models/models_tidb.go Normal file
View File

@@ -0,0 +1,18 @@
// +build tidb go1.4.2
// Copyright 2015 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.
package models
import (
_ "github.com/go-xorm/tidb"
"github.com/ngaut/log"
_ "github.com/pingcap/tidb"
)
func init() {
EnableTidb = true
log.SetLevelByString("error")
}

View File

@@ -1,106 +0,0 @@
// 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.
package models
import (
"errors"
"time"
)
type OauthType int
const (
GITHUB OauthType = iota + 1
GOOGLE
TWITTER
QQ
WEIBO
BITBUCKET
FACEBOOK
)
var (
ErrOauth2RecordNotExist = errors.New("OAuth2 record does not exist")
ErrOauth2NotAssociated = errors.New("OAuth2 is not associated with user")
)
type Oauth2 struct {
Id int64
Uid int64 `xorm:"unique(s)"` // userId
User *User `xorm:"-"`
Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google...
Identity string `xorm:"unique(s) unique(oauth)"` // id..
Token string `xorm:"TEXT not null"`
Created time.Time `xorm:"CREATED"`
Updated time.Time
HasRecentActivity bool `xorm:"-"`
}
func BindUserOauth2(userId, oauthId int64) error {
_, err := x.Id(oauthId).Update(&Oauth2{Uid: userId})
return err
}
func AddOauth2(oa *Oauth2) error {
_, err := x.Insert(oa)
return err
}
func GetOauth2(identity string) (oa *Oauth2, err error) {
oa = &Oauth2{Identity: identity}
isExist, err := x.Get(oa)
if err != nil {
return
} else if !isExist {
return nil, ErrOauth2RecordNotExist
} else if oa.Uid == -1 {
return oa, ErrOauth2NotAssociated
}
oa.User, err = GetUserByID(oa.Uid)
return oa, err
}
func GetOauth2ById(id int64) (oa *Oauth2, err error) {
oa = new(Oauth2)
has, err := x.Id(id).Get(oa)
if err != nil {
return nil, err
} else if !has {
return nil, ErrOauth2RecordNotExist
}
return oa, nil
}
// UpdateOauth2 updates given OAuth2.
func UpdateOauth2(oa *Oauth2) error {
_, err := x.Id(oa.Id).AllCols().Update(oa)
return err
}
// GetOauthByUserId returns list of oauthes that are related to given user.
func GetOauthByUserId(uid int64) ([]*Oauth2, error) {
socials := make([]*Oauth2, 0, 5)
err := x.Find(&socials, Oauth2{Uid: uid})
if err != nil {
return nil, err
}
for _, social := range socials {
social.HasRecentActivity = social.Updated.Add(7 * 24 * time.Hour).After(time.Now())
}
return socials, err
}
// DeleteOauth2ById deletes a oauth2 by ID.
func DeleteOauth2ById(id int64) error {
_, err := x.Delete(&Oauth2{Id: id})
return err
}
// CleanUnbindOauth deletes all unbind OAuthes.
func CleanUnbindOauth() error {
_, err := x.Delete(&Oauth2{Uid: -1})
return err
}

View File

@@ -10,7 +10,7 @@ import (
"os"
"strings"
"github.com/gogits/gogs/modules/base"
"github.com/go-xorm/xorm"
)
var (
@@ -93,17 +93,6 @@ func (org *User) RemoveOrgRepo(repoID int64) error {
return org.removeOrgRepo(x, repoID)
}
// IsOrgEmailUsed returns true if the e-mail has been used in organization account.
func IsOrgEmailUsed(email string) (bool, error) {
if len(email) == 0 {
return false, nil
}
return x.Get(&User{
Email: email,
Type: ORGANIZATION,
})
}
// CreateOrganization creates record of a new organization.
func CreateOrganization(org, owner *User) (err error) {
if err = IsUsableName(org.Name); err != nil {
@@ -117,18 +106,9 @@ func CreateOrganization(org, owner *User) (err error) {
return ErrUserAlreadyExist{org.Name}
}
isExist, err = IsOrgEmailUsed(org.Email)
if err != nil {
return err
} else if isExist {
return ErrEmailAlreadyUsed{org.Email}
}
org.LowerName = strings.ToLower(org.Name)
org.FullName = org.Name
org.Avatar = base.EncodeMd5(org.Email)
org.AvatarEmail = org.Email
// No password for organization.
org.UseCustomAvatar = true
org.NumTeams = 1
org.NumMembers = 1
@@ -141,6 +121,17 @@ func CreateOrganization(org, owner *User) (err error) {
if _, err = sess.Insert(org); err != nil {
return fmt.Errorf("insert organization: %v", err)
}
org.GenerateRandomAvatar()
// Add initial creator to organization and owner team.
if _, err = sess.Insert(&OrgUser{
Uid: owner.Id,
OrgID: org.Id,
IsOwner: true,
NumTeams: 1,
}); err != nil {
return fmt.Errorf("insert org-user relation: %v", err)
}
// Create default owner team.
t := &Team{
@@ -154,23 +145,11 @@ func CreateOrganization(org, owner *User) (err error) {
return fmt.Errorf("insert owner team: %v", err)
}
// Add initial creator to organization and owner team.
ou := &OrgUser{
Uid: owner.Id,
OrgID: org.Id,
IsOwner: true,
NumTeams: 1,
}
if _, err = sess.Insert(ou); err != nil {
return fmt.Errorf("insert org-user relation: %v", err)
}
tu := &TeamUser{
if _, err = sess.Insert(&TeamUser{
Uid: owner.Id,
OrgID: org.Id,
TeamID: t.ID,
}
if _, err = sess.Insert(tu); err != nil {
}); err != nil {
return fmt.Errorf("insert team-user relation: %v", err)
}
@@ -205,14 +184,12 @@ func CountOrganizations() int64 {
return count
}
// GetOrganizations returns given number of organizations with offset.
func GetOrganizations(num, offset int) ([]*User, error) {
orgs := make([]*User, 0, num)
err := x.Limit(num, offset).Where("type=1").Asc("id").Find(&orgs)
return orgs, err
// Organizations returns number of organizations in given page.
func Organizations(page, pageSize int) ([]*User, error) {
orgs := make([]*User, 0, pageSize)
return orgs, x.Limit(pageSize, (page-1)*pageSize).Where("type=1").Asc("id").Find(&orgs)
}
// TODO: need some kind of mechanism to record failure.
// DeleteOrganization completely and permanently deletes everything of organization.
func DeleteOrganization(org *User) (err error) {
if err := DeleteUser(org); err != nil {
@@ -220,23 +197,23 @@ func DeleteOrganization(org *User) (err error) {
}
sess := x.NewSession()
defer sess.Close()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Delete(&Team{OrgID: org.Id}); err != nil {
sess.Rollback()
return err
if err = deleteBeans(sess,
&Team{OrgID: org.Id},
&OrgUser{OrgID: org.Id},
&TeamUser{OrgID: org.Id},
); err != nil {
return fmt.Errorf("deleteBeans: %v", err)
}
if _, err = sess.Delete(&OrgUser{OrgID: org.Id}); err != nil {
sess.Rollback()
return err
}
if _, err = sess.Delete(&TeamUser{OrgID: org.Id}); err != nil {
sess.Rollback()
return err
if err = deleteUser(sess, org); err != nil {
return fmt.Errorf("deleteUser: %v", err)
}
return sess.Commit()
}
@@ -275,6 +252,25 @@ func IsPublicMembership(orgId, uid int64) bool {
return has
}
func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) {
orgs := make([]*User, 0, 10)
return orgs, sess.Where("`org_user`.uid=?", userID).And("`org_user`.is_owner=?", true).
Join("INNER", "`org_user`", "`org_user`.org_id=`user`.id").Find(&orgs)
}
// GetOwnedOrgsByUserID returns a list of organizations are owned by given user ID.
func GetOwnedOrgsByUserID(userID int64) ([]*User, error) {
sess := x.NewSession()
return getOwnedOrgsByUserID(sess, userID)
}
// GetOwnedOrganizationsByUserIDDesc returns a list of organizations are owned by
// given user ID and descring order by given condition.
func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
sess := x.NewSession()
return getOwnedOrgsByUserID(sess.Desc(desc), userID)
}
// GetOrgUsersByUserId returns all organization-user relations by user ID.
func GetOrgUsersByUserId(uid int64) ([]*OrgUser, error) {
ous := make([]*OrgUser, 0, 10)

View File

@@ -153,7 +153,7 @@ func parseKeyString(content string) (string, error) {
if len(lines) == 1 {
// Parse openssh format
parts := strings.Fields(lines[0])
parts := strings.SplitN(lines[0], " ", 3)
switch len(parts) {
case 0:
return "", errors.New("Empty key")

View File

@@ -272,6 +272,11 @@ func (repo *Repository) IsOwnedBy(userID int64) bool {
return repo.OwnerID == userID
}
// CanBeForked returns true if repository meets the requirements of being forked.
func (repo *Repository) CanBeForked() bool {
return !repo.IsBare && !repo.IsMirror
}
func (repo *Repository) NextIssueIndex() int64 {
return int64(repo.NumIssues+repo.NumPulls) + 1
}
@@ -465,6 +470,16 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str
return repo, fmt.Errorf("create update hook: %v", err)
}
// Check if repository is empty.
_, stderr, err = com.ExecCmdDir(repoPath, "git", "log", "-1")
if err != nil {
if strings.Contains(stderr, "fatal: bad default revision 'HEAD'") {
repo.IsBare = true
} else {
return repo, fmt.Errorf("check bare: %v - %s", err, stderr)
}
}
// Check if repository has master branch, if so set it to default branch.
gitRepo, err := git.OpenRepository(repoPath)
if err != nil {
@@ -615,7 +630,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C
}
tmpDir := filepath.Join(os.TempDir(), "gogs-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond()))
fmt.Println(tmpDir)
// Initialize repository according to user's choice.
if opts.AutoInit {
os.MkdirAll(tmpDir, os.ModePerm)
@@ -761,21 +776,16 @@ func CountPublicRepositories() int64 {
return countRepositories(false)
}
// GetRepositoriesWithUsers returns given number of repository objects with offset.
// It also auto-gets corresponding users.
func GetRepositoriesWithUsers(num, offset int) ([]*Repository, error) {
repos := make([]*Repository, 0, num)
if err := x.Limit(num, offset).Asc("id").Find(&repos); err != nil {
// RepositoriesWithUsers returns number of repos in given page.
func RepositoriesWithUsers(page, pageSize int) (_ []*Repository, err error) {
repos := make([]*Repository, 0, pageSize)
if err = x.Limit(pageSize, (page-1)*pageSize).Asc("id").Find(&repos); err != nil {
return nil, err
}
for _, repo := range repos {
repo.Owner = &User{Id: repo.OwnerID}
has, err := x.Get(repo.Owner)
if err != nil {
for i := range repos {
if err = repos[i].GetOwner(); err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{repo.OwnerID, ""}
}
}
@@ -1185,9 +1195,13 @@ func GetRecentUpdatedRepositories(page int) (repos []*Repository, err error) {
Where("is_private=?", false).Limit(setting.ExplorePagingNum).Desc("updated").Find(&repos)
}
func getRepositoryCount(e Engine, u *User) (int64, error) {
return x.Count(&Repository{OwnerID: u.Id})
}
// GetRepositoryCount returns the total number of repositories of user.
func GetRepositoryCount(u *User) (int64, error) {
return x.Count(&Repository{OwnerID: u.Id})
return getRepositoryCount(x, u)
}
type SearchOption struct {
@@ -1212,7 +1226,7 @@ func SearchRepositoryByName(opt SearchOption) (repos []*Repository, err error) {
sess.Where("owner_id=?", opt.Uid)
}
if !opt.Private {
sess.And("is_private=false")
sess.And("is_private=?", false)
}
sess.And("lower_name like ?", "%"+opt.Keyword+"%").Find(&repos)
return repos, err
@@ -1639,7 +1653,7 @@ func NotifyWatchers(act *Action) error {
type Star struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"uid UNIQUE(s)"`
UID int64 `xorm:"UNIQUE(s)"`
RepoID int64 `xorm:"UNIQUE(s)"`
}

View File

@@ -14,7 +14,7 @@ import (
// AccessToken represents a personal access token.
type AccessToken struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"uid INDEX"`
UID int64 `xorm:"INDEX"`
Name string
Sha1 string `xorm:"UNIQUE VARCHAR(40)"`
Created time.Time `xorm:"CREATED"`

View File

@@ -23,10 +23,6 @@ type UpdateTask struct {
NewCommitId string
}
const (
MAX_COMMITS int = 5
)
func AddUpdateTask(task *UpdateTask) error {
_, err := x.Insert(task)
return err
@@ -139,7 +135,6 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
var actEmail string
for e := l.Front(); e != nil; e = e.Next() {
commit := e.Value.(*git.Commit)
if actEmail == "" {
actEmail = commit.Committer.Email
}
@@ -147,10 +142,8 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
&base.PushCommit{commit.Id.String(),
commit.Message(),
commit.Author.Email,
commit.Author.Name})
if len(commits) >= MAX_COMMITS {
break
}
commit.Author.Name,
})
}
if err = CommitRepoAction(userId, ru.Id, userName, actEmail,

View File

@@ -55,12 +55,13 @@ type User struct {
Name string `xorm:"UNIQUE NOT NULL"`
FullName string
// Email is the primary email address (to be used for communication).
Email string `xorm:"UNIQUE(s) NOT NULL"`
Email string `xorm:"NOT NULL"`
Passwd string `xorm:"NOT NULL"`
LoginType LoginType
LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`
LoginName string
Type UserType `xorm:"UNIQUE(s)"`
Type UserType
OwnedOrgs []*User `xorm:"-"`
Orgs []*User `xorm:"-"`
Repos []*Repository `xorm:"-"`
Location string
@@ -109,8 +110,8 @@ func (u *User) AfterSet(colName string, _ xorm.Cell) {
// EmailAdresses is the list of all email addresses of a user. Can contain the
// primary email address, but is not obligatory
type EmailAddress struct {
Id int64
Uid int64 `xorm:"INDEX NOT NULL"`
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"INDEX NOT NULL"`
Email string `xorm:"UNIQUE NOT NULL"`
IsActivated bool
IsPrimary bool `xorm:"-"`
@@ -132,42 +133,72 @@ func (u *User) HomeLink() string {
return setting.AppSubUrl + "/" + u.Name
}
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
func (u *User) GenerateEmailActivateCode(email string) string {
code := base.CreateTimeLimitCode(
com.ToStr(u.Id)+email+u.LowerName+u.Passwd+u.Rands,
setting.Service.ActiveCodeLives, nil)
// Add tail hex username
code += hex.EncodeToString([]byte(u.LowerName))
return code
}
// GenerateActivateCode generates an activate code based on user information.
func (u *User) GenerateActivateCode() string {
return u.GenerateEmailActivateCode(u.Email)
}
// CustomAvatarPath returns user custom avatar file path.
func (u *User) CustomAvatarPath() string {
return filepath.Join(setting.AvatarUploadPath, com.ToStr(u.Id))
}
// GenerateRandomAvatar generates a random avatar for user.
func (u *User) GenerateRandomAvatar() error {
seed := u.Email
if len(seed) == 0 {
seed = u.Name
}
img, err := avatar.RandomImage([]byte(seed))
if err != nil {
return fmt.Errorf("RandomImage: %v", err)
}
if err = os.MkdirAll(path.Dir(u.CustomAvatarPath()), os.ModePerm); err != nil {
return fmt.Errorf("MkdirAll: %v", err)
}
fw, err := os.Create(u.CustomAvatarPath())
if err != nil {
return fmt.Errorf("Create: %v", err)
}
defer fw.Close()
if err = jpeg.Encode(fw, img, nil); err != nil {
return fmt.Errorf("Encode: %v", err)
}
log.Info("New random avatar created: %d", u.Id)
return nil
}
func (u *User) RelAvatarLink() string {
defaultImgUrl := "/img/avatar_default.jpg"
if u.Id == -1 {
return defaultImgUrl
}
imgPath := path.Join(setting.AvatarUploadPath, com.ToStr(u.Id))
switch {
case u.UseCustomAvatar:
if !com.IsExist(imgPath) {
if !com.IsExist(u.CustomAvatarPath()) {
return defaultImgUrl
}
return "/avatars/" + com.ToStr(u.Id)
case setting.DisableGravatar, setting.OfflineMode:
if !com.IsExist(imgPath) {
img, err := avatar.RandomImage([]byte(u.Email))
if err != nil {
log.Error(3, "RandomImage: %v", err)
return defaultImgUrl
if !com.IsExist(u.CustomAvatarPath()) {
if err := u.GenerateRandomAvatar(); err != nil {
log.Error(3, "GenerateRandomAvatar: %v", err)
}
if err = os.MkdirAll(path.Dir(imgPath), os.ModePerm); err != nil {
log.Error(3, "MkdirAll: %v", err)
return defaultImgUrl
}
fw, err := os.Create(imgPath)
if err != nil {
log.Error(3, "Create: %v", err)
return defaultImgUrl
}
defer fw.Close()
if err = jpeg.Encode(fw, img, nil); err != nil {
log.Error(3, "Encode: %v", err)
return defaultImgUrl
}
log.Info("New random avatar created: %d", u.Id)
}
return "/avatars/" + com.ToStr(u.Id)
@@ -208,11 +239,6 @@ func (u *User) ValidatePassword(passwd string) bool {
return u.Passwd == newUser.Passwd
}
// CustomAvatarPath returns user custom avatar file path.
func (u *User) CustomAvatarPath() string {
return filepath.Join(setting.AvatarUploadPath, com.ToStr(u.Id))
}
// UploadAvatar saves custom avatar for user.
// FIXME: split uploads to different subdirs in case we have massive users.
func (u *User) UploadAvatar(data []byte) error {
@@ -222,28 +248,27 @@ func (u *User) UploadAvatar(data []byte) error {
if err != nil {
return err
}
m := resize.Resize(234, 234, img, resize.NearestNeighbor)
sess := x.NewSession()
defer sess.Close()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Id(u.Id).AllCols().Update(u); err != nil {
sess.Rollback()
return err
}
os.MkdirAll(setting.AvatarUploadPath, os.ModePerm)
fw, err := os.Create(u.CustomAvatarPath())
if err != nil {
sess.Rollback()
return err
}
defer fw.Close()
if err = jpeg.Encode(fw, m, nil); err != nil {
sess.Rollback()
return err
}
@@ -284,9 +309,13 @@ func (u *User) IsPublicMember(orgId int64) bool {
return IsPublicMembership(orgId, u.Id)
}
func (u *User) getOrganizationCount(e Engine) (int64, error) {
return e.Where("uid=?", u.Id).Count(new(OrgUser))
}
// GetOrganizationCount returns count of membership of organization of user.
func (u *User) GetOrganizationCount() (int64, error) {
return x.Where("uid=?", u.Id).Count(new(OrgUser))
return u.getOrganizationCount(x)
}
// GetRepositories returns all repositories that user owns, including private repositories.
@@ -295,6 +324,12 @@ func (u *User) GetRepositories() (err error) {
return err
}
// GetOwnedOrganizations returns all organizations that user owns.
func (u *User) GetOwnedOrganizations() (err error) {
u.OwnedOrgs, err = GetOwnedOrgsByUserID(u.Id)
return err
}
// GetOrganizations returns all organizations that user belongs to.
func (u *User) GetOrganizations() error {
ous, err := GetOrgUsersByUserId(u.Id)
@@ -413,11 +448,10 @@ func CountUsers() int64 {
return countUsers(x)
}
// GetUsers returns given number of user objects with offset.
func GetUsers(num, offset int) ([]*User, error) {
users := make([]*User, 0, num)
err := x.Limit(num, offset).Where("type=0").Asc("id").Find(&users)
return users, err
// Users returns number of users in given page.
func Users(page, pageSize int) ([]*User, error) {
users := make([]*User, 0, pageSize)
return users, x.Limit(pageSize, (page-1)*pageSize).Where("type=0").Asc("id").Find(&users)
}
// get user by erify code
@@ -490,12 +524,20 @@ func ChangeUserName(u *User, newUserName string) (err error) {
}
func updateUser(e Engine, u *User) error {
u.Email = strings.ToLower(u.Email)
has, err := e.Where("id!=?", u.Id).And("type=?", u.Type).And("email=?", u.Email).Get(new(User))
if err != nil {
return err
} else if has {
return ErrEmailAlreadyUsed{u.Email}
// Organization does not need e-mail.
if !u.IsOrganization() {
u.Email = strings.ToLower(u.Email)
has, err := e.Where("id!=?", u.Id).And("type=?", u.Type).And("email=?", u.Email).Get(new(User))
if err != nil {
return err
} else if has {
return ErrEmailAlreadyUsed{u.Email}
}
if len(u.AvatarEmail) == 0 {
u.AvatarEmail = u.Email
}
u.Avatar = avatar.HashEmail(u.AvatarEmail)
}
u.LowerName = strings.ToLower(u.Name)
@@ -510,13 +552,8 @@ func updateUser(e Engine, u *User) error {
u.Description = u.Description[:255]
}
if u.AvatarEmail == "" {
u.AvatarEmail = u.Email
}
u.Avatar = avatar.HashEmail(u.AvatarEmail)
u.FullName = base.Sanitizer.Sanitize(u.FullName)
_, err = e.Id(u.Id).AllCols().Update(u)
_, err := e.Id(u.Id).AllCols().Update(u)
return err
}
@@ -525,8 +562,8 @@ func UpdateUser(u *User) error {
return updateUser(x, u)
}
// DeleteBeans deletes all given beans, beans should contain delete conditions.
func DeleteBeans(e Engine, beans ...interface{}) (err error) {
// deleteBeans deletes all given beans, beans should contain delete conditions.
func deleteBeans(e Engine, beans ...interface{}) (err error) {
for i := range beans {
if _, err = e.Delete(beans[i]); err != nil {
return err
@@ -536,14 +573,12 @@ func DeleteBeans(e Engine, beans ...interface{}) (err error) {
}
// FIXME: need some kind of mechanism to record failure. HINT: system notice
// DeleteUser completely and permanently deletes everything of a user,
// but issues/comments/pulls will be kept and shown as someone has been deleted.
func DeleteUser(u *User) error {
func deleteUser(e *xorm.Session, u *User) error {
// Note: A user owns any repository or belongs to any organization
// cannot perform delete operation.
// Check ownership of repository.
count, err := GetRepositoryCount(u)
count, err := getRepositoryCount(e, u)
if err != nil {
return fmt.Errorf("GetRepositoryCount: %v", err)
} else if count > 0 {
@@ -551,26 +586,20 @@ func DeleteUser(u *User) error {
}
// Check membership of organization.
count, err = u.GetOrganizationCount()
count, err = u.getOrganizationCount(e)
if err != nil {
return fmt.Errorf("GetOrganizationCount: %v", err)
} else if count > 0 {
return ErrUserHasOrgs{UID: u.Id}
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
// ***** START: Watch *****
watches := make([]*Watch, 0, 10)
if err = x.Find(&watches, &Watch{UserID: u.Id}); err != nil {
if err = e.Find(&watches, &Watch{UserID: u.Id}); err != nil {
return fmt.Errorf("get all watches: %v", err)
}
for i := range watches {
if _, err = sess.Exec("UPDATE `repository` SET num_watches=num_watches-1 WHERE id=?", watches[i].RepoID); err != nil {
if _, err = e.Exec("UPDATE `repository` SET num_watches=num_watches-1 WHERE id=?", watches[i].RepoID); err != nil {
return fmt.Errorf("decrease repository watch number[%d]: %v", watches[i].RepoID, err)
}
}
@@ -578,11 +607,11 @@ func DeleteUser(u *User) error {
// ***** START: Star *****
stars := make([]*Star, 0, 10)
if err = x.Find(&stars, &Star{UID: u.Id}); err != nil {
if err = e.Find(&stars, &Star{UID: u.Id}); err != nil {
return fmt.Errorf("get all stars: %v", err)
}
for i := range stars {
if _, err = sess.Exec("UPDATE `repository` SET num_stars=num_stars-1 WHERE id=?", stars[i].RepoID); err != nil {
if _, err = e.Exec("UPDATE `repository` SET num_stars=num_stars-1 WHERE id=?", stars[i].RepoID); err != nil {
return fmt.Errorf("decrease repository star number[%d]: %v", stars[i].RepoID, err)
}
}
@@ -590,18 +619,17 @@ func DeleteUser(u *User) error {
// ***** START: Follow *****
followers := make([]*Follow, 0, 10)
if err = x.Find(&followers, &Follow{UserID: u.Id}); err != nil {
if err = e.Find(&followers, &Follow{UserID: u.Id}); err != nil {
return fmt.Errorf("get all followers: %v", err)
}
for i := range followers {
if _, err = sess.Exec("UPDATE `user` SET num_followers=num_followers-1 WHERE id=?", followers[i].UserID); err != nil {
if _, err = e.Exec("UPDATE `user` SET num_followers=num_followers-1 WHERE id=?", followers[i].UserID); err != nil {
return fmt.Errorf("decrease user follower number[%d]: %v", followers[i].UserID, err)
}
}
// ***** END: Follow *****
if err = DeleteBeans(sess,
&Oauth2{Uid: u.Id},
if err = deleteBeans(e,
&AccessToken{UID: u.Id},
&Collaboration{UserID: u.Id},
&Access{UserID: u.Id},
@@ -610,36 +638,32 @@ func DeleteUser(u *User) error {
&Follow{FollowID: u.Id},
&Action{UserID: u.Id},
&IssueUser{UID: u.Id},
&EmailAddress{Uid: u.Id},
&EmailAddress{UID: u.Id},
); err != nil {
return fmt.Errorf("DeleteBeans: %v", err)
return fmt.Errorf("deleteUser: %v", err)
}
// ***** START: PublicKey *****
keys := make([]*PublicKey, 0, 10)
if err = sess.Find(&keys, &PublicKey{OwnerID: u.Id}); err != nil {
if err = e.Find(&keys, &PublicKey{OwnerID: u.Id}); err != nil {
return fmt.Errorf("get all public keys: %v", err)
}
for _, key := range keys {
if err = deletePublicKey(sess, key.ID); err != nil {
if err = deletePublicKey(e, key.ID); err != nil {
return fmt.Errorf("deletePublicKey: %v", err)
}
}
// ***** END: PublicKey *****
// Clear assignee.
if _, err = sess.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.Id); err != nil {
if _, err = e.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.Id); err != nil {
return fmt.Errorf("clear assignee: %v", err)
}
if _, err = sess.Id(u.Id).Delete(new(User)); err != nil {
if _, err = e.Id(u.Id).Delete(new(User)); err != nil {
return fmt.Errorf("Delete: %v", err)
}
if err = sess.Commit(); err != nil {
return fmt.Errorf("Commit: %v", err)
}
// FIXME: system notice
// Note: There are something just cannot be roll back,
// so just keep error logs of those operations.
@@ -651,6 +675,23 @@ func DeleteUser(u *User) error {
return nil
}
// DeleteUser completely and permanently deletes everything of a user,
// but issues/comments/pulls will be kept and shown as someone has been deleted.
func DeleteUser(u *User) (err error) {
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if err = deleteUser(sess, u); err != nil {
// Note: don't wrapper error here.
return err
}
return sess.Commit()
}
// DeleteInactivateUsers deletes all inactivate users and email addresses.
func DeleteInactivateUsers() (err error) {
users := make([]*User, 0, 10)
@@ -805,11 +846,11 @@ func AddEmailAddress(email *EmailAddress) error {
func (email *EmailAddress) Activate() error {
email.IsActivated = true
if _, err := x.Id(email.Id).AllCols().Update(email); err != nil {
if _, err := x.Id(email.ID).AllCols().Update(email); err != nil {
return err
}
if user, err := GetUserByID(email.Uid); err != nil {
if user, err := GetUserByID(email.UID); err != nil {
return err
} else {
user.Rands = GetUserSalt()
@@ -825,7 +866,7 @@ func DeleteEmailAddress(email *EmailAddress) error {
return ErrEmailNotExist
}
if _, err = x.Id(email.Id).Delete(email); err != nil {
if _, err = x.Id(email.ID).Delete(email); err != nil {
return err
}
@@ -845,12 +886,12 @@ func MakeEmailPrimary(email *EmailAddress) error {
return ErrEmailNotActivated
}
user := &User{Id: email.Uid}
user := &User{Id: email.UID}
has, err = x.Get(user)
if err != nil {
return err
} else if !has {
return ErrUserNotExist{email.Uid, ""}
return ErrUserNotExist{email.UID, ""}
}
// Make sure the former primary email doesn't disappear
@@ -859,7 +900,7 @@ func MakeEmailPrimary(email *EmailAddress) error {
if err != nil {
return err
} else if !has {
former_primary_email.Uid = user.Id
former_primary_email.UID = user.Id
former_primary_email.IsActivated = user.IsActive
x.Insert(former_primary_email)
}
@@ -936,7 +977,7 @@ func GetUserByEmail(email string) (*User, error) {
return nil, err
}
if has {
return GetUserByID(emailAddress.Uid)
return GetUserByID(emailAddress.UID)
}
return nil, ErrUserNotExist{0, "email"}

View File

@@ -10,17 +10,30 @@ import (
"github.com/macaron-contrib/binding"
)
type AdminCrateUserForm struct {
LoginType string `binding:"Required"`
LoginName string
UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"MaxSize(255)"`
SendNotify bool
}
func (f *AdminCrateUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
type AdminEditUserForm struct {
FullName string `form:"fullname" binding:"MaxSize(100)"`
LoginType string `binding:"Required"`
LoginName string
FullName string `binding:"MaxSize(100)"`
Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"OmitEmpty;MinSize(6);MaxSize(255)"`
Password string `binding:"MaxSize(255)"`
Website string `binding:"MaxSize(50)"`
Location string `binding:"MaxSize(50)"`
Avatar string `binding:"Required;Email;MaxSize(50)"`
Active bool
Admin bool
AllowGitHook bool
LoginType int
}
func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

View File

@@ -10,28 +10,28 @@ import (
)
type AuthenticationForm struct {
ID int64 `form:"id"`
Type int
Name string `binding:"Required;MaxSize(50)"`
Host string
Port int
UseSSL bool `form:"use_ssl"`
BindDN string `form:"bind_dn"`
BindPassword string
UserBase string
AttributeName string
AttributeSurname string
AttributeMail string
Filter string
AdminFilter string
IsActived bool
SMTPAuth string `form:"smtp_auth"`
SMTPHost string `form:"smtp_host"`
SMTPPort int `form:"smtp_port"`
TLS bool `form:"tls"`
SkipVerify bool
AllowAutoRegister bool `form:"allowautoregister"`
PAMServiceName string
ID int64
Type int `binding:"Range(2,5)"`
Name string `binding:"Required;MaxSize(30)"`
Host string
Port int
BindDN string
BindPassword string
UserBase string
UserDN string `form:"user_dn"`
AttributeName string
AttributeSurname string
AttributeMail string
Filter string
AdminFilter string
IsActive bool
SMTPAuth string
SMTPHost string
SMTPPort int
AllowedDomains string
TLS bool
SkipVerify bool
PAMServiceName string `form:"pam_service_name"`
}
func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

View File

@@ -4,61 +4,98 @@ Gogs LDAP Authentication Module
## About
This authentication module attempts to authorize and authenticate a user
against an LDAP server. Like most LDAP authentication systems, this module does
this in two steps. First, it queries the LDAP server using a Bind DN and
searches for the user that is attempting to sign in. If the user is found, the
module attempts to bind to the server using the user's supplied credentials. If
this succeeds, the user has been authenticated, and his account information is
retrieved and passed to the Gogs login infrastructure.
against an LDAP server. It provides two methods of authentication: LDAP via
BindDN, and LDAP simple authentication.
LDAP via BindDN functions like most LDAP authentication systems. First, it
queries the LDAP server using a Bind DN and searches for the user that is
attempting to sign in. If the user is found, the module attempts to bind to the
server using the user's supplied credentials. If this succeeds, the user has
been authenticated, and his account information is retrieved and passed to the
Gogs login infrastructure.
LDAP simple authentication does not utilize a Bind DN. Instead, it binds
directly with the LDAP server using the user's supplied credentials. If the bind
succeeds and no filter rules out the user, the user is authenticated.
LDAP via BindDN is recommended for most users. By using a Bind DN, the server
can perform authorization by restricting which entries the Bind DN account can
read. Further, using a Bind DN with reduced permissions can reduce security risk
in the face of application bugs.
## Usage
To use this module, add an LDAP authentication source via the Authentications
section in the admin panel. The fields should be set as follows:
section in the admin panel. Both the LDAP via BindDN and the simple auth LDAP
share the following fields:
* Authorization Name **(required)**
* A name to assign to the new method of authorization.
* A name to assign to the new method of authorization.
* Host **(required)**
* The address where the LDAP server can be reached.
* Example: mydomain.com
* The address where the LDAP server can be reached.
* Example: mydomain.com
* Port **(required)**
* The port to use when connecting to the server.
* Example: 636
* The port to use when connecting to the server.
* Example: 636
* Enable TLS Encryption (optional)
* Whether to use TLS when connecting to the LDAP server.
* Whether to use TLS when connecting to the LDAP server.
* Bind DN (optional)
* The DN to bind to the LDAP server with when searching for the user.
This may be left blank to perform an anonymous search.
* Example: cn=Search,dc=mydomain,dc=com
* Bind Password (optional)
* The password for the Bind DN specified above, if any.
* User Search Base **(required)**
* The LDAP base at which user accounts will be searched for.
* Example: ou=Users,dc=mydomain,dc=com
* User Filter **(required)**
* An LDAP filter declaring how to find the user record that is attempting
to authenticate. The '%s' matching parameter will be substituted with
the user's username.
* Example: (&(objectClass=posixAccount)(uid=%s))
* Admin Filter (optional)
* An LDAP filter specifying if a user should be given administrator
privileges. If a user accounts passes the filter, the user will be
privileged as an administrator.
* Example: (objectClass=adminAccount)
* First name attribute (optional)
* The attribute of the user's LDAP record containing the user's first
name. This will be used to populate their account information.
* Example: givenName
* The attribute of the user's LDAP record containing the user's first name.
This will be used to populate their account information.
* Example: givenName
* Surname name attribute (optional)
* The attribute of the user's LDAP record containing the user's surname
This will be used to populate their account information.
* Example: sn
* Surname attribute (optional)
* The attribute of the user's LDAP record containing the user's surname This
will be used to populate their account information.
* Example: sn
* E-mail attribute **(required)**
* The attribute of the user's LDAP record containing the user's email
address. This will be used to populate their account information.
* Example: mail
* The attribute of the user's LDAP record containing the user's email
address. This will be used to populate their account information.
* Example: mail
**LDAP via BindDN** adds the following fields:
* Bind DN (optional)
* The DN to bind to the LDAP server with when searching for the user. This
may be left blank to perform an anonymous search.
* Example: cn=Search,dc=mydomain,dc=com
* Bind Password (optional)
* The password for the Bind DN specified above, if any. _Note: The password
is stored in plaintext at the server. As such, ensure that your Bind DN
has as few privileges as possible._
* User Search Base **(required)**
* The LDAP base at which user accounts will be searched for.
* Example: ou=Users,dc=mydomain,dc=com
* User Filter **(required)**
* An LDAP filter declaring how to find the user record that is attempting to
authenticate. The '%s' matching parameter will be substituted with the
user's username.
* Example: (&(objectClass=posixAccount)(uid=%s))
**LDAP using simple auth** adds the following fields:
* User DN **(required)**
* A template to use as the user's DN. The `%s` matching parameter will be
substituted with the user's username.
* Example: cn=%s,ou=Users,dc=mydomain,dc=com
* Example: uid=%s,ou=Users,dc=mydomain,dc=com
* User Filter **(required)**
* An LDAP filter declaring when a user should be allowed to log in. The `%s`
matching parameter will be substituted with the user's username.
* Example: (&(objectClass=posixAccount)(cn=%s))
* Example: (&(objectClass=posixAccount)(uid=%s))

View File

@@ -7,6 +7,7 @@
package ldap
import (
"crypto/tls"
"fmt"
"github.com/gogits/gogs/modules/ldap"
@@ -14,14 +15,16 @@ import (
)
// Basic LDAP authentication service
type Ldapsource struct {
type Source struct {
Name string // canonical name (ie. corporate.ad)
Host string // LDAP host
Port int // port number
UseSSL bool // Use SSL
SkipVerify bool
BindDN string // DN to bind with
BindPassword string // Bind DN password
UserBase string // Base search path for users
UserDN string // Template for the DN of the user for simple auth
AttributeName string // First name attribute
AttributeSurname string // Surname attribute
AttributeMail string // E-mail attribute
@@ -30,7 +33,7 @@ type Ldapsource struct {
Enabled bool // if this source is disabled
}
func (ls Ldapsource) FindUserDN(name string) (string, bool) {
func (ls *Source) FindUserDN(name string) (string, bool) {
l, err := ldapDial(ls)
if err != nil {
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
@@ -78,10 +81,19 @@ func (ls Ldapsource) FindUserDN(name string) (string, bool) {
}
// searchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
func (ls Ldapsource) SearchEntry(name, passwd string) (string, string, string, bool, bool) {
userDN, found := ls.FindUserDN(name)
if !found {
return "", "", "", false, false
func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, bool, bool) {
var userDN string
if directBind {
log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
userDN = fmt.Sprintf(ls.UserDN, name)
} else {
log.Trace("LDAP will use BindDN.")
var found bool
userDN, found = ls.FindUserDN(name)
if !found {
return "", "", "", false, false
}
}
l, err := ldapDial(ls)
@@ -90,7 +102,6 @@ func (ls Ldapsource) SearchEntry(name, passwd string) (string, string, string, b
ls.Enabled = false
return "", "", "", false, false
}
defer l.Close()
log.Trace("Binding with userDN: %s", userDN)
@@ -112,7 +123,12 @@ func (ls Ldapsource) SearchEntry(name, passwd string) (string, string, string, b
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
return "", "", "", false, false
} else if len(sr.Entries) < 1 {
log.Error(4, "LDAP Search failed unexpectedly! (0 entries)")
if directBind {
log.Error(4, "User filter inhibited user login.")
} else {
log.Error(4, "LDAP Search failed unexpectedly! (0 entries)")
}
return "", "", "", false, false
}
@@ -140,10 +156,12 @@ func (ls Ldapsource) SearchEntry(name, passwd string) (string, string, string, b
return name_attr, sn_attr, mail_attr, admin_attr, true
}
func ldapDial(ls Ldapsource) (*ldap.Conn, error) {
func ldapDial(ls *Source) (*ldap.Conn, error) {
if ls.UseSSL {
log.Debug("Using TLS for LDAP")
return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), nil)
log.Debug("Using TLS for LDAP without verifying: %v", ls.SkipVerify)
return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), &tls.Config{
InsecureSkipVerify: ls.SkipVerify,
})
} else {
return ldap.Dial("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port))
}

View File

@@ -17,8 +17,7 @@ import (
// \/ /_____/ \/ \/ \/ \/ \/
type CreateOrgForm struct {
OrgName string `form:"org_name" binding:"Required;AlphaDashDot;MaxSize(30)"`
Email string `form:"email" binding:"Required;Email;MaxSize(50)"`
OrgName string `binding:"Required;AlphaDashDot;MaxSize(30)" locale:"org.org_name_holder"`
}
func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
@@ -26,13 +25,11 @@ func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) bind
}
type UpdateOrgSettingForm struct {
OrgUserName string `form:"uname" binding:"Required;AlphaDashDot;MaxSize(30)" locale:"org.org_name_holder"`
OrgFullName string `form:"fullname" binding:"MaxSize(100)"`
Email string `form:"email" binding:"Required;Email;MaxSize(50)"`
Description string `form:"desc" binding:"MaxSize(255)"`
Website string `form:"website" binding:"Url;MaxSize(100)"`
Location string `form:"location" binding:"MaxSize(50)"`
Avatar string `form:"avatar" binding:"Required;Email;MaxSize(50)"`
Name string `binding:"Required;AlphaDashDot;MaxSize(30)" locale:"org.org_name_holder"`
FullName string `binding:"MaxSize(100)"`
Description string `binding:"MaxSize(255)"`
Website string `binding:"Url;MaxSize(100)"`
Location string `binding:"MaxSize(50)"`
}
func (f *UpdateOrgSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

View File

@@ -38,6 +38,7 @@ type InstallForm struct {
OfflineMode bool
DisableGravatar bool
DisableRegistration bool
EnableCaptcha bool
RequireSignInView bool
AdminName string `binding:"OmitEmpty;AlphaDashDot;MaxSize(30)" locale:"install.admin_name"`
@@ -58,12 +59,10 @@ func (f *InstallForm) Validate(ctx *macaron.Context, errs binding.Errors) bindin
// \/ \/
type RegisterForm struct {
UserName string `form:"uname" binding:"Required;AlphaDashDot;MaxSize(35)"`
Email string `form:"email" binding:"Required;Email;MaxSize(254)"`
Password string `form:"password" binding:"Required;MaxSize(255)"`
Retype string `form:"retype"`
LoginType string `form:"logintype"`
LoginName string `form:"loginname"`
UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"Required;MaxSize(255)"`
Retype string
}
func (f *RegisterForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
@@ -71,9 +70,9 @@ func (f *RegisterForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi
}
type SignInForm struct {
UserName string `form:"uname" binding:"Required;MaxSize(254)"`
Password string `form:"password" binding:"Required;MaxSize(255)"`
Remember bool `form:"remember"`
UserName string `binding:"Required;MaxSize(254)"`
Password string `binding:"Required;MaxSize(255)"`
Remember bool
}
func (f *SignInForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
@@ -88,12 +87,12 @@ func (f *SignInForm) Validate(ctx *macaron.Context, errs binding.Errors) binding
// \/ \/ \/ \/ \/
type UpdateProfileForm struct {
UserName string `form:"uname" binding:"Required;MaxSize(35)"`
FullName string `form:"fullname" binding:"MaxSize(100)"`
Email string `form:"email" binding:"Required;Email;MaxSize(254)"`
Website string `form:"website" binding:"Url;MaxSize(100)"`
Location string `form:"location" binding:"MaxSize(50)"`
Avatar string `form:"avatar" binding:"Required;Email;MaxSize(254)"`
Name string `binding:"Required;MaxSize(35)"`
FullName string `binding:"MaxSize(100)"`
Email string `binding:"Required;Email;MaxSize(254)"`
Website string `binding:"Url;MaxSize(100)"`
Location string `binding:"MaxSize(50)"`
Gravatar string `binding:"Required;Email;MaxSize(254)"`
}
func (f *UpdateProfileForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
@@ -101,8 +100,8 @@ func (f *UpdateProfileForm) Validate(ctx *macaron.Context, errs binding.Errors)
}
type UploadAvatarForm struct {
Enable bool `form:"enable"`
Avatar *multipart.FileHeader `form:"avatar"`
Enable bool
Avatar *multipart.FileHeader
}
func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
@@ -110,7 +109,7 @@ func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) b
}
type AddEmailForm struct {
Email string `binding:"Required;Email;MaxSize(50)"`
Email string `binding:"Required;Email;MaxSize(254)"`
}
func (f *AddEmailForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

View File

@@ -76,9 +76,8 @@ func ToUtf8WithErr(content []byte) (error, string) {
}
encoding, _ := charset.Lookup(charsetLabel)
if encoding == nil {
return fmt.Errorf("unknow char decoder %s", charsetLabel), string(content)
return fmt.Errorf("unknown char decoder %s", charsetLabel), string(content)
}
result, n, err := transform.String(encoding.NewDecoder(), string(content))
@@ -97,13 +96,42 @@ func ToUtf8(content string) string {
return res
}
// RenderCommitMessage renders commit message with XSS-safe and special links.
func RenderCommitMessage(msg, urlPrefix string) template.HTML {
return template.HTML(string(RenderIssueIndexPattern([]byte(template.HTMLEscapeString(msg)), urlPrefix)))
// Replaces all prefixes 'old' in 's' with 'new'.
func ReplaceLeft(s, old, new string) string {
old_len, new_len, i, n := len(old), len(new), 0, 0
for ; i < len(s) && strings.HasPrefix(s[i:], old); n += 1 {
i += old_len
}
// simple optimization
if n == 0 {
return s
}
// allocating space for the new string
newLen := n*new_len + len(s[i:])
replacement := make([]byte, newLen, newLen)
j := 0
for ; j < n*new_len; j += new_len {
copy(replacement[j:j+new_len], new)
}
copy(replacement[j:], s[i:])
return string(replacement)
}
var mailDomains = map[string]string{
"gmail.com": "gmail.com",
// RenderCommitMessage renders commit message with XSS-safe and special links.
func RenderCommitMessage(msg, urlPrefix string) template.HTML {
cleanMsg := template.HTMLEscapeString(msg)
fullMessage := string(RenderIssueIndexPattern([]byte(cleanMsg), urlPrefix))
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
for i := range msgLines {
msgLines[i] = ReplaceLeft(msgLines[i], " ", "&nbsp;")
}
fullMessage = strings.Join(msgLines, "<br>")
return template.HTML(fullMessage)
}
var TemplateFuncs template.FuncMap = map[string]interface{}{
@@ -151,12 +179,7 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
return "try.gogs.io"
}
suffix := strings.SplitN(mail, "@", 2)[1]
domain, ok := mailDomains[suffix]
if !ok {
return "mail." + suffix
}
return domain
return strings.SplitN(mail, "@", 2)[1]
},
"SubStr": func(str string, start, length int) string {
if len(str) == 0 {

View File

@@ -15,6 +15,7 @@ import (
"hash"
"html/template"
"math"
"regexp"
"strings"
"time"
@@ -26,7 +27,7 @@ import (
"github.com/gogits/gogs/modules/setting"
)
var Sanitizer = bluemonday.UGCPolicy()
var Sanitizer = bluemonday.UGCPolicy().AllowAttrs("class").Matching(regexp.MustCompile(`[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*`)).OnElements("code")
// Encode string to md5 hex value.
func EncodeMd5(str string) string {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,615 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package agent implements a client to an ssh-agent daemon.
References:
[PROTOCOL.agent]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent?rev=HEAD
*/
package agent
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"sync"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// Agent represents the capabilities of an ssh-agent.
type Agent interface {
// List returns the identities known to the agent.
List() ([]*Key, error)
// Sign has the agent sign the data using a protocol 2 key as defined
// in [PROTOCOL.agent] section 2.6.2.
Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error)
// Add adds a private key to the agent.
Add(key AddedKey) error
// Remove removes all identities with the given public key.
Remove(key ssh.PublicKey) error
// RemoveAll removes all identities.
RemoveAll() error
// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list.
Lock(passphrase []byte) error
// Unlock undoes the effect of Lock
Unlock(passphrase []byte) error
// Signers returns signers for all the known keys.
Signers() ([]ssh.Signer, error)
}
// AddedKey describes an SSH key to be added to an Agent.
type AddedKey struct {
// PrivateKey must be a *rsa.PrivateKey, *dsa.PrivateKey or
// *ecdsa.PrivateKey, which will be inserted into the agent.
PrivateKey interface{}
// Certificate, if not nil, is communicated to the agent and will be
// stored with the key.
Certificate *ssh.Certificate
// Comment is an optional, free-form string.
Comment string
// LifetimeSecs, if not zero, is the number of seconds that the
// agent will store the key for.
LifetimeSecs uint32
// ConfirmBeforeUse, if true, requests that the agent confirm with the
// user before each use of this key.
ConfirmBeforeUse bool
}
// See [PROTOCOL.agent], section 3.
const (
agentRequestV1Identities = 1
// 3.2 Requests from client to agent for protocol 2 key operations
agentAddIdentity = 17
agentRemoveIdentity = 18
agentRemoveAllIdentities = 19
agentAddIdConstrained = 25
// 3.3 Key-type independent requests from client to agent
agentAddSmartcardKey = 20
agentRemoveSmartcardKey = 21
agentLock = 22
agentUnlock = 23
agentAddSmartcardKeyConstrained = 26
// 3.7 Key constraint identifiers
agentConstrainLifetime = 1
agentConstrainConfirm = 2
)
// maxAgentResponseBytes is the maximum agent reply size that is accepted. This
// is a sanity check, not a limit in the spec.
const maxAgentResponseBytes = 16 << 20
// Agent messages:
// These structures mirror the wire format of the corresponding ssh agent
// messages found in [PROTOCOL.agent].
// 3.4 Generic replies from agent to client
const agentFailure = 5
type failureAgentMsg struct{}
const agentSuccess = 6
type successAgentMsg struct{}
// See [PROTOCOL.agent], section 2.5.2.
const agentRequestIdentities = 11
type requestIdentitiesAgentMsg struct{}
// See [PROTOCOL.agent], section 2.5.2.
const agentIdentitiesAnswer = 12
type identitiesAnswerAgentMsg struct {
NumKeys uint32 `sshtype:"12"`
Keys []byte `ssh:"rest"`
}
// See [PROTOCOL.agent], section 2.6.2.
const agentSignRequest = 13
type signRequestAgentMsg struct {
KeyBlob []byte `sshtype:"13"`
Data []byte
Flags uint32
}
// See [PROTOCOL.agent], section 2.6.2.
// 3.6 Replies from agent to client for protocol 2 key operations
const agentSignResponse = 14
type signResponseAgentMsg struct {
SigBlob []byte `sshtype:"14"`
}
type publicKey struct {
Format string
Rest []byte `ssh:"rest"`
}
// Key represents a protocol 2 public key as defined in
// [PROTOCOL.agent], section 2.5.2.
type Key struct {
Format string
Blob []byte
Comment string
}
func clientErr(err error) error {
return fmt.Errorf("agent: client error: %v", err)
}
// String returns the storage form of an agent key with the format, base64
// encoded serialized key, and the comment if it is not empty.
func (k *Key) String() string {
s := string(k.Format) + " " + base64.StdEncoding.EncodeToString(k.Blob)
if k.Comment != "" {
s += " " + k.Comment
}
return s
}
// Type returns the public key type.
func (k *Key) Type() string {
return k.Format
}
// Marshal returns key blob to satisfy the ssh.PublicKey interface.
func (k *Key) Marshal() []byte {
return k.Blob
}
// Verify satisfies the ssh.PublicKey interface, but is not
// implemented for agent keys.
func (k *Key) Verify(data []byte, sig *ssh.Signature) error {
return errors.New("agent: agent key does not know how to verify")
}
type wireKey struct {
Format string
Rest []byte `ssh:"rest"`
}
func parseKey(in []byte) (out *Key, rest []byte, err error) {
var record struct {
Blob []byte
Comment string
Rest []byte `ssh:"rest"`
}
if err := ssh.Unmarshal(in, &record); err != nil {
return nil, nil, err
}
var wk wireKey
if err := ssh.Unmarshal(record.Blob, &wk); err != nil {
return nil, nil, err
}
return &Key{
Format: wk.Format,
Blob: record.Blob,
Comment: record.Comment,
}, record.Rest, nil
}
// client is a client for an ssh-agent process.
type client struct {
// conn is typically a *net.UnixConn
conn io.ReadWriter
// mu is used to prevent concurrent access to the agent
mu sync.Mutex
}
// NewClient returns an Agent that talks to an ssh-agent process over
// the given connection.
func NewClient(rw io.ReadWriter) Agent {
return &client{conn: rw}
}
// call sends an RPC to the agent. On success, the reply is
// unmarshaled into reply and replyType is set to the first byte of
// the reply, which contains the type of the message.
func (c *client) call(req []byte) (reply interface{}, err error) {
c.mu.Lock()
defer c.mu.Unlock()
msg := make([]byte, 4+len(req))
binary.BigEndian.PutUint32(msg, uint32(len(req)))
copy(msg[4:], req)
if _, err = c.conn.Write(msg); err != nil {
return nil, clientErr(err)
}
var respSizeBuf [4]byte
if _, err = io.ReadFull(c.conn, respSizeBuf[:]); err != nil {
return nil, clientErr(err)
}
respSize := binary.BigEndian.Uint32(respSizeBuf[:])
if respSize > maxAgentResponseBytes {
return nil, clientErr(err)
}
buf := make([]byte, respSize)
if _, err = io.ReadFull(c.conn, buf); err != nil {
return nil, clientErr(err)
}
reply, err = unmarshal(buf)
if err != nil {
return nil, clientErr(err)
}
return reply, err
}
func (c *client) simpleCall(req []byte) error {
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
func (c *client) RemoveAll() error {
return c.simpleCall([]byte{agentRemoveAllIdentities})
}
func (c *client) Remove(key ssh.PublicKey) error {
req := ssh.Marshal(&agentRemoveIdentityMsg{
KeyBlob: key.Marshal(),
})
return c.simpleCall(req)
}
func (c *client) Lock(passphrase []byte) error {
req := ssh.Marshal(&agentLockMsg{
Passphrase: passphrase,
})
return c.simpleCall(req)
}
func (c *client) Unlock(passphrase []byte) error {
req := ssh.Marshal(&agentUnlockMsg{
Passphrase: passphrase,
})
return c.simpleCall(req)
}
// List returns the identities known to the agent.
func (c *client) List() ([]*Key, error) {
// see [PROTOCOL.agent] section 2.5.2.
req := []byte{agentRequestIdentities}
msg, err := c.call(req)
if err != nil {
return nil, err
}
switch msg := msg.(type) {
case *identitiesAnswerAgentMsg:
if msg.NumKeys > maxAgentResponseBytes/8 {
return nil, errors.New("agent: too many keys in agent reply")
}
keys := make([]*Key, msg.NumKeys)
data := msg.Keys
for i := uint32(0); i < msg.NumKeys; i++ {
var key *Key
var err error
if key, data, err = parseKey(data); err != nil {
return nil, err
}
keys[i] = key
}
return keys, nil
case *failureAgentMsg:
return nil, errors.New("agent: failed to list keys")
}
panic("unreachable")
}
// Sign has the agent sign the data using a protocol 2 key as defined
// in [PROTOCOL.agent] section 2.6.2.
func (c *client) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
req := ssh.Marshal(signRequestAgentMsg{
KeyBlob: key.Marshal(),
Data: data,
})
msg, err := c.call(req)
if err != nil {
return nil, err
}
switch msg := msg.(type) {
case *signResponseAgentMsg:
var sig ssh.Signature
if err := ssh.Unmarshal(msg.SigBlob, &sig); err != nil {
return nil, err
}
return &sig, nil
case *failureAgentMsg:
return nil, errors.New("agent: failed to sign challenge")
}
panic("unreachable")
}
// unmarshal parses an agent message in packet, returning the parsed
// form and the message type of packet.
func unmarshal(packet []byte) (interface{}, error) {
if len(packet) < 1 {
return nil, errors.New("agent: empty packet")
}
var msg interface{}
switch packet[0] {
case agentFailure:
return new(failureAgentMsg), nil
case agentSuccess:
return new(successAgentMsg), nil
case agentIdentitiesAnswer:
msg = new(identitiesAnswerAgentMsg)
case agentSignResponse:
msg = new(signResponseAgentMsg)
default:
return nil, fmt.Errorf("agent: unknown type tag %d", packet[0])
}
if err := ssh.Unmarshal(packet, msg); err != nil {
return nil, err
}
return msg, nil
}
type rsaKeyMsg struct {
Type string `sshtype:"17"`
N *big.Int
E *big.Int
D *big.Int
Iqmp *big.Int // IQMP = Inverse Q Mod P
P *big.Int
Q *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type dsaKeyMsg struct {
Type string `sshtype:"17"`
P *big.Int
Q *big.Int
G *big.Int
Y *big.Int
X *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ecdsaKeyMsg struct {
Type string `sshtype:"17"`
Curve string
KeyBytes []byte
D *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
// Insert adds a private key to the agent.
func (c *client) insertKey(s interface{}, comment string, constraints []byte) error {
var req []byte
switch k := s.(type) {
case *rsa.PrivateKey:
if len(k.Primes) != 2 {
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
}
k.Precompute()
req = ssh.Marshal(rsaKeyMsg{
Type: ssh.KeyAlgoRSA,
N: k.N,
E: big.NewInt(int64(k.E)),
D: k.D,
Iqmp: k.Precomputed.Qinv,
P: k.Primes[0],
Q: k.Primes[1],
Comments: comment,
Constraints: constraints,
})
case *dsa.PrivateKey:
req = ssh.Marshal(dsaKeyMsg{
Type: ssh.KeyAlgoDSA,
P: k.P,
Q: k.Q,
G: k.G,
Y: k.Y,
X: k.X,
Comments: comment,
Constraints: constraints,
})
case *ecdsa.PrivateKey:
nistID := fmt.Sprintf("nistp%d", k.Params().BitSize)
req = ssh.Marshal(ecdsaKeyMsg{
Type: "ecdsa-sha2-" + nistID,
Curve: nistID,
KeyBytes: elliptic.Marshal(k.Curve, k.X, k.Y),
D: k.D,
Comments: comment,
Constraints: constraints,
})
default:
return fmt.Errorf("agent: unsupported key type %T", s)
}
// if constraints are present then the message type needs to be changed.
if len(constraints) != 0 {
req[0] = agentAddIdConstrained
}
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
type rsaCertMsg struct {
Type string `sshtype:"17"`
CertBytes []byte
D *big.Int
Iqmp *big.Int // IQMP = Inverse Q Mod P
P *big.Int
Q *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type dsaCertMsg struct {
Type string `sshtype:"17"`
CertBytes []byte
X *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ecdsaCertMsg struct {
Type string `sshtype:"17"`
CertBytes []byte
D *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
// Insert adds a private key to the agent. If a certificate is given,
// that certificate is added instead as public key.
func (c *client) Add(key AddedKey) error {
var constraints []byte
if secs := key.LifetimeSecs; secs != 0 {
constraints = append(constraints, agentConstrainLifetime)
var secsBytes [4]byte
binary.BigEndian.PutUint32(secsBytes[:], secs)
constraints = append(constraints, secsBytes[:]...)
}
if key.ConfirmBeforeUse {
constraints = append(constraints, agentConstrainConfirm)
}
if cert := key.Certificate; cert == nil {
return c.insertKey(key.PrivateKey, key.Comment, constraints)
} else {
return c.insertCert(key.PrivateKey, cert, key.Comment, constraints)
}
}
func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string, constraints []byte) error {
var req []byte
switch k := s.(type) {
case *rsa.PrivateKey:
if len(k.Primes) != 2 {
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
}
k.Precompute()
req = ssh.Marshal(rsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
D: k.D,
Iqmp: k.Precomputed.Qinv,
P: k.Primes[0],
Q: k.Primes[1],
Comments: comment,
Constraints: constraints,
})
case *dsa.PrivateKey:
req = ssh.Marshal(dsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
X: k.X,
Comments: comment,
})
case *ecdsa.PrivateKey:
req = ssh.Marshal(ecdsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
D: k.D,
Comments: comment,
})
default:
return fmt.Errorf("agent: unsupported key type %T", s)
}
// if constraints are present then the message type needs to be changed.
if len(constraints) != 0 {
req[0] = agentAddIdConstrained
}
signer, err := ssh.NewSignerFromKey(s)
if err != nil {
return err
}
if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 {
return errors.New("agent: signer and cert have different public key")
}
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
// Signers provides a callback for client authentication.
func (c *client) Signers() ([]ssh.Signer, error) {
keys, err := c.List()
if err != nil {
return nil, err
}
var result []ssh.Signer
for _, k := range keys {
result = append(result, &agentKeyringSigner{c, k})
}
return result, nil
}
type agentKeyringSigner struct {
agent *client
pub ssh.PublicKey
}
func (s *agentKeyringSigner) PublicKey() ssh.PublicKey {
return s.pub
}
func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
// The agent has its own entropy source, so the rand argument is ignored.
return s.agent.Sign(s.pub, data)
}

View File

@@ -0,0 +1,287 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"bytes"
"crypto/rand"
"errors"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// startAgent executes ssh-agent, and returns a Agent interface to it.
func startAgent(t *testing.T) (client Agent, socket string, cleanup func()) {
if testing.Short() {
// ssh-agent is not always available, and the key
// types supported vary by platform.
t.Skip("skipping test due to -short")
}
bin, err := exec.LookPath("ssh-agent")
if err != nil {
t.Skip("could not find ssh-agent")
}
cmd := exec.Command(bin, "-s")
out, err := cmd.Output()
if err != nil {
t.Fatalf("cmd.Output: %v", err)
}
/* Output looks like:
SSH_AUTH_SOCK=/tmp/ssh-P65gpcqArqvH/agent.15541; export SSH_AUTH_SOCK;
SSH_AGENT_PID=15542; export SSH_AGENT_PID;
echo Agent pid 15542;
*/
fields := bytes.Split(out, []byte(";"))
line := bytes.SplitN(fields[0], []byte("="), 2)
line[0] = bytes.TrimLeft(line[0], "\n")
if string(line[0]) != "SSH_AUTH_SOCK" {
t.Fatalf("could not find key SSH_AUTH_SOCK in %q", fields[0])
}
socket = string(line[1])
line = bytes.SplitN(fields[2], []byte("="), 2)
line[0] = bytes.TrimLeft(line[0], "\n")
if string(line[0]) != "SSH_AGENT_PID" {
t.Fatalf("could not find key SSH_AGENT_PID in %q", fields[2])
}
pidStr := line[1]
pid, err := strconv.Atoi(string(pidStr))
if err != nil {
t.Fatalf("Atoi(%q): %v", pidStr, err)
}
conn, err := net.Dial("unix", string(socket))
if err != nil {
t.Fatalf("net.Dial: %v", err)
}
ac := NewClient(conn)
return ac, socket, func() {
proc, _ := os.FindProcess(pid)
if proc != nil {
proc.Kill()
}
conn.Close()
os.RemoveAll(filepath.Dir(socket))
}
}
func testAgent(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
agent, _, cleanup := startAgent(t)
defer cleanup()
testAgentInterface(t, agent, key, cert, lifetimeSecs)
}
func testAgentInterface(t *testing.T, agent Agent, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
t.Fatalf("NewSignerFromKey(%T): %v", key, err)
}
// The agent should start up empty.
if keys, err := agent.List(); err != nil {
t.Fatalf("RequestIdentities: %v", err)
} else if len(keys) > 0 {
t.Fatalf("got %d keys, want 0: %v", len(keys), keys)
}
// Attempt to insert the key, with certificate if specified.
var pubKey ssh.PublicKey
if cert != nil {
err = agent.Add(AddedKey{
PrivateKey: key,
Certificate: cert,
Comment: "comment",
LifetimeSecs: lifetimeSecs,
})
pubKey = cert
} else {
err = agent.Add(AddedKey{PrivateKey: key, Comment: "comment", LifetimeSecs: lifetimeSecs})
pubKey = signer.PublicKey()
}
if err != nil {
t.Fatalf("insert(%T): %v", key, err)
}
// Did the key get inserted successfully?
if keys, err := agent.List(); err != nil {
t.Fatalf("List: %v", err)
} else if len(keys) != 1 {
t.Fatalf("got %v, want 1 key", keys)
} else if keys[0].Comment != "comment" {
t.Fatalf("key comment: got %v, want %v", keys[0].Comment, "comment")
} else if !bytes.Equal(keys[0].Blob, pubKey.Marshal()) {
t.Fatalf("key mismatch")
}
// Can the agent make a valid signature?
data := []byte("hello")
sig, err := agent.Sign(pubKey, data)
if err != nil {
t.Fatalf("Sign(%s): %v", pubKey.Type(), err)
}
if err := pubKey.Verify(data, sig); err != nil {
t.Fatalf("Verify(%s): %v", pubKey.Type(), err)
}
}
func TestAgent(t *testing.T) {
for _, keyType := range []string{"rsa", "dsa", "ecdsa"} {
testAgent(t, testPrivateKeys[keyType], nil, 0)
}
}
func TestCert(t *testing.T) {
cert := &ssh.Certificate{
Key: testPublicKeys["rsa"],
ValidBefore: ssh.CertTimeInfinity,
CertType: ssh.UserCert,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
testAgent(t, testPrivateKeys["rsa"], cert, 0)
}
func TestConstraints(t *testing.T) {
testAgent(t, testPrivateKeys["rsa"], nil, 3600 /* lifetime in seconds */)
}
// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and
// therefore is buffered (net.Pipe deadlocks if both sides start with
// a write.)
func netPipe() (net.Conn, net.Conn, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, nil, err
}
defer listener.Close()
c1, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
return nil, nil, err
}
c2, err := listener.Accept()
if err != nil {
c1.Close()
return nil, nil, err
}
return c1, c2, nil
}
func TestAuth(t *testing.T) {
a, b, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer a.Close()
defer b.Close()
agent, _, cleanup := startAgent(t)
defer cleanup()
if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"], Comment: "comment"}); err != nil {
t.Errorf("Add: %v", err)
}
serverConf := ssh.ServerConfig{}
serverConf.AddHostKey(testSigners["rsa"])
serverConf.PublicKeyCallback = func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("pubkey rejected")
}
go func() {
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
if err != nil {
t.Fatalf("Server: %v", err)
}
conn.Close()
}()
conf := ssh.ClientConfig{}
conf.Auth = append(conf.Auth, ssh.PublicKeysCallback(agent.Signers))
conn, _, _, err := ssh.NewClientConn(b, "", &conf)
if err != nil {
t.Fatalf("NewClientConn: %v", err)
}
conn.Close()
}
func TestLockClient(t *testing.T) {
agent, _, cleanup := startAgent(t)
defer cleanup()
testLockAgent(agent, t)
}
func testLockAgent(agent Agent, t *testing.T) {
if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"], Comment: "comment 1"}); err != nil {
t.Errorf("Add: %v", err)
}
if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["dsa"], Comment: "comment dsa"}); err != nil {
t.Errorf("Add: %v", err)
}
if keys, err := agent.List(); err != nil {
t.Errorf("List: %v", err)
} else if len(keys) != 2 {
t.Errorf("Want 2 keys, got %v", keys)
}
passphrase := []byte("secret")
if err := agent.Lock(passphrase); err != nil {
t.Errorf("Lock: %v", err)
}
if keys, err := agent.List(); err != nil {
t.Errorf("List: %v", err)
} else if len(keys) != 0 {
t.Errorf("Want 0 keys, got %v", keys)
}
signer, _ := ssh.NewSignerFromKey(testPrivateKeys["rsa"])
if _, err := agent.Sign(signer.PublicKey(), []byte("hello")); err == nil {
t.Fatalf("Sign did not fail")
}
if err := agent.Remove(signer.PublicKey()); err == nil {
t.Fatalf("Remove did not fail")
}
if err := agent.RemoveAll(); err == nil {
t.Fatalf("RemoveAll did not fail")
}
if err := agent.Unlock(nil); err == nil {
t.Errorf("Unlock with wrong passphrase succeeded")
}
if err := agent.Unlock(passphrase); err != nil {
t.Errorf("Unlock: %v", err)
}
if err := agent.Remove(signer.PublicKey()); err != nil {
t.Fatalf("Remove: %v", err)
}
if keys, err := agent.List(); err != nil {
t.Errorf("List: %v", err)
} else if len(keys) != 1 {
t.Errorf("Want 1 keys, got %v", keys)
}
}

View File

@@ -0,0 +1,103 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"errors"
"io"
"net"
"sync"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// RequestAgentForwarding sets up agent forwarding for the session.
// ForwardToAgent or ForwardToRemote should be called to route
// the authentication requests.
func RequestAgentForwarding(session *ssh.Session) error {
ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil)
if err != nil {
return err
}
if !ok {
return errors.New("forwarding request denied")
}
return nil
}
// ForwardToAgent routes authentication requests to the given keyring.
func ForwardToAgent(client *ssh.Client, keyring Agent) error {
channels := client.HandleChannelOpen(channelType)
if channels == nil {
return errors.New("agent: already have handler for " + channelType)
}
go func() {
for ch := range channels {
channel, reqs, err := ch.Accept()
if err != nil {
continue
}
go ssh.DiscardRequests(reqs)
go func() {
ServeAgent(keyring, channel)
channel.Close()
}()
}
}()
return nil
}
const channelType = "auth-agent@openssh.com"
// ForwardToRemote routes authentication requests to the ssh-agent
// process serving on the given unix socket.
func ForwardToRemote(client *ssh.Client, addr string) error {
channels := client.HandleChannelOpen(channelType)
if channels == nil {
return errors.New("agent: already have handler for " + channelType)
}
conn, err := net.Dial("unix", addr)
if err != nil {
return err
}
conn.Close()
go func() {
for ch := range channels {
channel, reqs, err := ch.Accept()
if err != nil {
continue
}
go ssh.DiscardRequests(reqs)
go forwardUnixSocket(channel, addr)
}
}()
return nil
}
func forwardUnixSocket(channel ssh.Channel, addr string) {
conn, err := net.Dial("unix", addr)
if err != nil {
return
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(conn, channel)
conn.(*net.UnixConn).CloseWrite()
wg.Done()
}()
go func() {
io.Copy(channel, conn)
channel.CloseWrite()
wg.Done()
}()
wg.Wait()
conn.Close()
channel.Close()
}

View File

@@ -0,0 +1,184 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"bytes"
"crypto/rand"
"crypto/subtle"
"errors"
"fmt"
"sync"
"github.com/gogits/gogs/modules/crypto/ssh"
)
type privKey struct {
signer ssh.Signer
comment string
}
type keyring struct {
mu sync.Mutex
keys []privKey
locked bool
passphrase []byte
}
var errLocked = errors.New("agent: locked")
// NewKeyring returns an Agent that holds keys in memory. It is safe
// for concurrent use by multiple goroutines.
func NewKeyring() Agent {
return &keyring{}
}
// RemoveAll removes all identities.
func (r *keyring) RemoveAll() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
r.keys = nil
return nil
}
// Remove removes all identities with the given public key.
func (r *keyring) Remove(key ssh.PublicKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
want := key.Marshal()
found := false
for i := 0; i < len(r.keys); {
if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) {
found = true
r.keys[i] = r.keys[len(r.keys)-1]
r.keys = r.keys[len(r.keys)-1:]
continue
} else {
i++
}
}
if !found {
return errors.New("agent: key not found")
}
return nil
}
// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list.
func (r *keyring) Lock(passphrase []byte) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
r.locked = true
r.passphrase = passphrase
return nil
}
// Unlock undoes the effect of Lock
func (r *keyring) Unlock(passphrase []byte) error {
r.mu.Lock()
defer r.mu.Unlock()
if !r.locked {
return errors.New("agent: not locked")
}
if len(passphrase) != len(r.passphrase) || 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
return fmt.Errorf("agent: incorrect passphrase")
}
r.locked = false
r.passphrase = nil
return nil
}
// List returns the identities known to the agent.
func (r *keyring) List() ([]*Key, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
// section 2.7: locked agents return empty.
return nil, nil
}
var ids []*Key
for _, k := range r.keys {
pub := k.signer.PublicKey()
ids = append(ids, &Key{
Format: pub.Type(),
Blob: pub.Marshal(),
Comment: k.comment})
}
return ids, nil
}
// Insert adds a private key to the keyring. If a certificate
// is given, that certificate is added as public key. Note that
// any constraints given are ignored.
func (r *keyring) Add(key AddedKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
if err != nil {
return err
}
if cert := key.Certificate; cert != nil {
signer, err = ssh.NewCertSigner(cert, signer)
if err != nil {
return err
}
}
r.keys = append(r.keys, privKey{signer, key.Comment})
return nil
}
// Sign returns a signature for the data.
func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return nil, errLocked
}
wanted := key.Marshal()
for _, k := range r.keys {
if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) {
return k.signer.Sign(rand.Reader, data)
}
}
return nil, errors.New("not found")
}
// Signers returns signers for all the known keys.
func (r *keyring) Signers() ([]ssh.Signer, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return nil, errLocked
}
s := make([]ssh.Signer, 0, len(r.keys))
for _, k := range r.keys {
s = append(s, k.signer)
}
return s, nil
}

View File

@@ -0,0 +1,209 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"crypto/rsa"
"encoding/binary"
"fmt"
"io"
"log"
"math/big"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// Server wraps an Agent and uses it to implement the agent side of
// the SSH-agent, wire protocol.
type server struct {
agent Agent
}
func (s *server) processRequestBytes(reqData []byte) []byte {
rep, err := s.processRequest(reqData)
if err != nil {
if err != errLocked {
// TODO(hanwen): provide better logging interface?
log.Printf("agent %d: %v", reqData[0], err)
}
return []byte{agentFailure}
}
if err == nil && rep == nil {
return []byte{agentSuccess}
}
return ssh.Marshal(rep)
}
func marshalKey(k *Key) []byte {
var record struct {
Blob []byte
Comment string
}
record.Blob = k.Marshal()
record.Comment = k.Comment
return ssh.Marshal(&record)
}
type agentV1IdentityMsg struct {
Numkeys uint32 `sshtype:"2"`
}
type agentRemoveIdentityMsg struct {
KeyBlob []byte `sshtype:"18"`
}
type agentLockMsg struct {
Passphrase []byte `sshtype:"22"`
}
type agentUnlockMsg struct {
Passphrase []byte `sshtype:"23"`
}
func (s *server) processRequest(data []byte) (interface{}, error) {
switch data[0] {
case agentRequestV1Identities:
return &agentV1IdentityMsg{0}, nil
case agentRemoveIdentity:
var req agentRemoveIdentityMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
var wk wireKey
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
return nil, err
}
return nil, s.agent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob})
case agentRemoveAllIdentities:
return nil, s.agent.RemoveAll()
case agentLock:
var req agentLockMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
return nil, s.agent.Lock(req.Passphrase)
case agentUnlock:
var req agentLockMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
return nil, s.agent.Unlock(req.Passphrase)
case agentSignRequest:
var req signRequestAgentMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
var wk wireKey
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
return nil, err
}
k := &Key{
Format: wk.Format,
Blob: req.KeyBlob,
}
sig, err := s.agent.Sign(k, req.Data) // TODO(hanwen): flags.
if err != nil {
return nil, err
}
return &signResponseAgentMsg{SigBlob: ssh.Marshal(sig)}, nil
case agentRequestIdentities:
keys, err := s.agent.List()
if err != nil {
return nil, err
}
rep := identitiesAnswerAgentMsg{
NumKeys: uint32(len(keys)),
}
for _, k := range keys {
rep.Keys = append(rep.Keys, marshalKey(k)...)
}
return rep, nil
case agentAddIdentity:
return nil, s.insertIdentity(data)
}
return nil, fmt.Errorf("unknown opcode %d", data[0])
}
func (s *server) insertIdentity(req []byte) error {
var record struct {
Type string `sshtype:"17"`
Rest []byte `ssh:"rest"`
}
if err := ssh.Unmarshal(req, &record); err != nil {
return err
}
switch record.Type {
case ssh.KeyAlgoRSA:
var k rsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return err
}
priv := rsa.PrivateKey{
PublicKey: rsa.PublicKey{
E: int(k.E.Int64()),
N: k.N,
},
D: k.D,
Primes: []*big.Int{k.P, k.Q},
}
priv.Precompute()
return s.agent.Add(AddedKey{PrivateKey: &priv, Comment: k.Comments})
}
return fmt.Errorf("not implemented: %s", record.Type)
}
// ServeAgent serves the agent protocol on the given connection. It
// returns when an I/O error occurs.
func ServeAgent(agent Agent, c io.ReadWriter) error {
s := &server{agent}
var length [4]byte
for {
if _, err := io.ReadFull(c, length[:]); err != nil {
return err
}
l := binary.BigEndian.Uint32(length[:])
if l > maxAgentResponseBytes {
// We also cap requests.
return fmt.Errorf("agent: request too large: %d", l)
}
req := make([]byte, l)
if _, err := io.ReadFull(c, req); err != nil {
return err
}
repData := s.processRequestBytes(req)
if len(repData) > maxAgentResponseBytes {
return fmt.Errorf("agent: reply too large: %d bytes", len(repData))
}
binary.BigEndian.PutUint32(length[:], uint32(len(repData)))
if _, err := c.Write(length[:]); err != nil {
return err
}
if _, err := c.Write(repData); err != nil {
return err
}
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"testing"
"github.com/gogits/gogs/modules/crypto/ssh"
)
func TestServer(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
client := NewClient(c1)
go ServeAgent(NewKeyring(), c2)
testAgentInterface(t, client, testPrivateKeys["rsa"], nil, 0)
}
func TestLockServer(t *testing.T) {
testLockAgent(NewKeyring(), t)
}
func TestSetupForwardAgent(t *testing.T) {
a, b, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer a.Close()
defer b.Close()
_, socket, cleanup := startAgent(t)
defer cleanup()
serverConf := ssh.ServerConfig{
NoClientAuth: true,
}
serverConf.AddHostKey(testSigners["rsa"])
incoming := make(chan *ssh.ServerConn, 1)
go func() {
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
if err != nil {
t.Fatalf("Server: %v", err)
}
incoming <- conn
}()
conf := ssh.ClientConfig{}
conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf)
if err != nil {
t.Fatalf("NewClientConn: %v", err)
}
client := ssh.NewClient(conn, chans, reqs)
if err := ForwardToRemote(client, socket); err != nil {
t.Fatalf("SetupForwardAgent: %v", err)
}
server := <-incoming
ch, reqs, err := server.OpenChannel(channelType, nil)
if err != nil {
t.Fatalf("OpenChannel(%q): %v", channelType, err)
}
go ssh.DiscardRequests(reqs)
agentClient := NewClient(ch)
testAgentInterface(t, agentClient, testPrivateKeys["rsa"], nil, 0)
conn.Close()
}

View File

@@ -0,0 +1,64 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// IMPLEMENTOR NOTE: To avoid a package loop, this file is in three places:
// ssh/, ssh/agent, and ssh/test/. It should be kept in sync across all three
// instances.
package agent
import (
"crypto/rand"
"fmt"
"github.com/gogits/gogs/modules/crypto/ssh"
"github.com/gogits/gogs/modules/crypto/ssh/testdata"
)
var (
testPrivateKeys map[string]interface{}
testSigners map[string]ssh.Signer
testPublicKeys map[string]ssh.PublicKey
)
func init() {
var err error
n := len(testdata.PEMBytes)
testPrivateKeys = make(map[string]interface{}, n)
testSigners = make(map[string]ssh.Signer, n)
testPublicKeys = make(map[string]ssh.PublicKey, n)
for t, k := range testdata.PEMBytes {
testPrivateKeys[t], err = ssh.ParseRawPrivateKey(k)
if err != nil {
panic(fmt.Sprintf("Unable to parse test key %s: %v", t, err))
}
testSigners[t], err = ssh.NewSignerFromKey(testPrivateKeys[t])
if err != nil {
panic(fmt.Sprintf("Unable to create signer for test key %s: %v", t, err))
}
testPublicKeys[t] = testSigners[t].PublicKey()
}
// Create a cert and sign it for use in tests.
testCert := &ssh.Certificate{
Nonce: []byte{}, // To pass reflect.DeepEqual after marshal & parse, this must be non-nil
ValidPrincipals: []string{"gopher1", "gopher2"}, // increases test coverage
ValidAfter: 0, // unix epoch
ValidBefore: ssh.CertTimeInfinity, // The end of currently representable time.
Reserved: []byte{}, // To pass reflect.DeepEqual after marshal & parse, this must be non-nil
Key: testPublicKeys["ecdsa"],
SignatureKey: testPublicKeys["rsa"],
Permissions: ssh.Permissions{
CriticalOptions: map[string]string{},
Extensions: map[string]string{},
},
}
testCert.SignCert(rand.Reader, testSigners["rsa"])
testPrivateKeys["cert"] = testPrivateKeys["ecdsa"]
testSigners["cert"], err = ssh.NewCertSigner(testCert, testSigners["ecdsa"])
if err != nil {
panic(fmt.Sprintf("Unable to create certificate signer: %v", err))
}
}

View File

@@ -0,0 +1,122 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"errors"
"io"
"net"
"testing"
)
type server struct {
*ServerConn
chans <-chan NewChannel
}
func newServer(c net.Conn, conf *ServerConfig) (*server, error) {
sconn, chans, reqs, err := NewServerConn(c, conf)
if err != nil {
return nil, err
}
go DiscardRequests(reqs)
return &server{sconn, chans}, nil
}
func (s *server) Accept() (NewChannel, error) {
n, ok := <-s.chans
if !ok {
return nil, io.EOF
}
return n, nil
}
func sshPipe() (Conn, *server, error) {
c1, c2, err := netPipe()
if err != nil {
return nil, nil, err
}
clientConf := ClientConfig{
User: "user",
}
serverConf := ServerConfig{
NoClientAuth: true,
}
serverConf.AddHostKey(testSigners["ecdsa"])
done := make(chan *server, 1)
go func() {
server, err := newServer(c2, &serverConf)
if err != nil {
done <- nil
}
done <- server
}()
client, _, reqs, err := NewClientConn(c1, "", &clientConf)
if err != nil {
return nil, nil, err
}
server := <-done
if server == nil {
return nil, nil, errors.New("server handshake failed.")
}
go DiscardRequests(reqs)
return client, server, nil
}
func BenchmarkEndToEnd(b *testing.B) {
b.StopTimer()
client, server, err := sshPipe()
if err != nil {
b.Fatalf("sshPipe: %v", err)
}
defer client.Close()
defer server.Close()
size := (1 << 20)
input := make([]byte, size)
output := make([]byte, size)
b.SetBytes(int64(size))
done := make(chan int, 1)
go func() {
newCh, err := server.Accept()
if err != nil {
b.Fatalf("Client: %v", err)
}
ch, incoming, err := newCh.Accept()
go DiscardRequests(incoming)
for i := 0; i < b.N; i++ {
if _, err := io.ReadFull(ch, output); err != nil {
b.Fatalf("ReadFull: %v", err)
}
}
ch.Close()
done <- 1
}()
ch, in, err := client.OpenChannel("speed", nil)
if err != nil {
b.Fatalf("OpenChannel: %v", err)
}
go DiscardRequests(in)
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
if _, err := ch.Write(input); err != nil {
b.Fatalf("WriteFull: %v", err)
}
}
ch.Close()
b.StopTimer()
<-done
}

98
modules/crypto/ssh/buffer.go Executable file
View File

@@ -0,0 +1,98 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"sync"
)
// buffer provides a linked list buffer for data exchange
// between producer and consumer. Theoretically the buffer is
// of unlimited capacity as it does no allocation of its own.
type buffer struct {
// protects concurrent access to head, tail and closed
*sync.Cond
head *element // the buffer that will be read first
tail *element // the buffer that will be read last
closed bool
}
// An element represents a single link in a linked list.
type element struct {
buf []byte
next *element
}
// newBuffer returns an empty buffer that is not closed.
func newBuffer() *buffer {
e := new(element)
b := &buffer{
Cond: newCond(),
head: e,
tail: e,
}
return b
}
// write makes buf available for Read to receive.
// buf must not be modified after the call to write.
func (b *buffer) write(buf []byte) {
b.Cond.L.Lock()
e := &element{buf: buf}
b.tail.next = e
b.tail = e
b.Cond.Signal()
b.Cond.L.Unlock()
}
// eof closes the buffer. Reads from the buffer once all
// the data has been consumed will receive os.EOF.
func (b *buffer) eof() error {
b.Cond.L.Lock()
b.closed = true
b.Cond.Signal()
b.Cond.L.Unlock()
return nil
}
// Read reads data from the internal buffer in buf. Reads will block
// if no data is available, or until the buffer is closed.
func (b *buffer) Read(buf []byte) (n int, err error) {
b.Cond.L.Lock()
defer b.Cond.L.Unlock()
for len(buf) > 0 {
// if there is data in b.head, copy it
if len(b.head.buf) > 0 {
r := copy(buf, b.head.buf)
buf, b.head.buf = buf[r:], b.head.buf[r:]
n += r
continue
}
// if there is a next buffer, make it the head
if len(b.head.buf) == 0 && b.head != b.tail {
b.head = b.head.next
continue
}
// if at least one byte has been copied, return
if n > 0 {
break
}
// if nothing was read, and there is nothing outstanding
// check to see if the buffer is closed.
if b.closed {
err = io.EOF
break
}
// out of buffers, wait for producer
b.Cond.Wait()
}
return
}

View File

@@ -0,0 +1,87 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"testing"
)
var alphabet = []byte("abcdefghijklmnopqrstuvwxyz")
func TestBufferReadwrite(t *testing.T) {
b := newBuffer()
b.write(alphabet[:10])
r, _ := b.Read(make([]byte, 10))
if r != 10 {
t.Fatalf("Expected written == read == 10, written: 10, read %d", r)
}
b = newBuffer()
b.write(alphabet[:5])
r, _ = b.Read(make([]byte, 10))
if r != 5 {
t.Fatalf("Expected written == read == 5, written: 5, read %d", r)
}
b = newBuffer()
b.write(alphabet[:10])
r, _ = b.Read(make([]byte, 5))
if r != 5 {
t.Fatalf("Expected written == 10, read == 5, written: 10, read %d", r)
}
b = newBuffer()
b.write(alphabet[:5])
b.write(alphabet[5:15])
r, _ = b.Read(make([]byte, 10))
r2, _ := b.Read(make([]byte, 10))
if r != 10 || r2 != 5 || 15 != r+r2 {
t.Fatal("Expected written == read == 15")
}
}
func TestBufferClose(t *testing.T) {
b := newBuffer()
b.write(alphabet[:10])
b.eof()
_, err := b.Read(make([]byte, 5))
if err != nil {
t.Fatal("expected read of 5 to not return EOF")
}
b = newBuffer()
b.write(alphabet[:10])
b.eof()
r, err := b.Read(make([]byte, 5))
r2, err2 := b.Read(make([]byte, 10))
if r != 5 || r2 != 5 || err != nil || err2 != nil {
t.Fatal("expected reads of 5 and 5")
}
b = newBuffer()
b.write(alphabet[:10])
b.eof()
r, err = b.Read(make([]byte, 5))
r2, err2 = b.Read(make([]byte, 10))
r3, err3 := b.Read(make([]byte, 10))
if r != 5 || r2 != 5 || r3 != 0 || err != nil || err2 != nil || err3 != io.EOF {
t.Fatal("expected reads of 5 and 5 and 0, with EOF")
}
b = newBuffer()
b.write(make([]byte, 5))
b.write(make([]byte, 10))
b.eof()
r, err = b.Read(make([]byte, 9))
r2, err2 = b.Read(make([]byte, 3))
r3, err3 = b.Read(make([]byte, 3))
r4, err4 := b.Read(make([]byte, 10))
if err != nil || err2 != nil || err3 != nil || err4 != io.EOF {
t.Fatalf("Expected EOF on forth read only, err=%v, err2=%v, err3=%v, err4=%v", err, err2, err3, err4)
}
if r != 9 || r2 != 3 || r3 != 3 || r4 != 0 {
t.Fatal("Expected written == read == 15", r, r2, r3, r4)
}
}

501
modules/crypto/ssh/certs.go Executable file
View File

@@ -0,0 +1,501 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"sort"
"time"
)
// These constants from [PROTOCOL.certkeys] represent the algorithm names
// for certificate types supported by this package.
const (
CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com"
CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
)
// Certificate types distinguish between host and user
// certificates. The values can be set in the CertType field of
// Certificate.
const (
UserCert = 1
HostCert = 2
)
// Signature represents a cryptographic signature.
type Signature struct {
Format string
Blob []byte
}
// CertTimeInfinity can be used for OpenSSHCertV01.ValidBefore to indicate that
// a certificate does not expire.
const CertTimeInfinity = 1<<64 - 1
// An Certificate represents an OpenSSH certificate as defined in
// [PROTOCOL.certkeys]?rev=1.8.
type Certificate struct {
Nonce []byte
Key PublicKey
Serial uint64
CertType uint32
KeyId string
ValidPrincipals []string
ValidAfter uint64
ValidBefore uint64
Permissions
Reserved []byte
SignatureKey PublicKey
Signature *Signature
}
// genericCertData holds the key-independent part of the certificate data.
// Overall, certificates contain an nonce, public key fields and
// key-independent fields.
type genericCertData struct {
Serial uint64
CertType uint32
KeyId string
ValidPrincipals []byte
ValidAfter uint64
ValidBefore uint64
CriticalOptions []byte
Extensions []byte
Reserved []byte
SignatureKey []byte
Signature []byte
}
func marshalStringList(namelist []string) []byte {
var to []byte
for _, name := range namelist {
s := struct{ N string }{name}
to = append(to, Marshal(&s)...)
}
return to
}
type optionsTuple struct {
Key string
Value []byte
}
type optionsTupleValue struct {
Value string
}
// serialize a map of critical options or extensions
// issue #10569 - per [PROTOCOL.certkeys] and SSH implementation,
// we need two length prefixes for a non-empty string value
func marshalTuples(tups map[string]string) []byte {
keys := make([]string, 0, len(tups))
for key := range tups {
keys = append(keys, key)
}
sort.Strings(keys)
var ret []byte
for _, key := range keys {
s := optionsTuple{Key: key}
if value := tups[key]; len(value) > 0 {
s.Value = Marshal(&optionsTupleValue{value})
}
ret = append(ret, Marshal(&s)...)
}
return ret
}
// issue #10569 - per [PROTOCOL.certkeys] and SSH implementation,
// we need two length prefixes for a non-empty option value
func parseTuples(in []byte) (map[string]string, error) {
tups := map[string]string{}
var lastKey string
var haveLastKey bool
for len(in) > 0 {
var key, val, extra []byte
var ok bool
if key, in, ok = parseString(in); !ok {
return nil, errShortRead
}
keyStr := string(key)
// according to [PROTOCOL.certkeys], the names must be in
// lexical order.
if haveLastKey && keyStr <= lastKey {
return nil, fmt.Errorf("ssh: certificate options are not in lexical order")
}
lastKey, haveLastKey = keyStr, true
// the next field is a data field, which if non-empty has a string embedded
if val, in, ok = parseString(in); !ok {
return nil, errShortRead
}
if len(val) > 0 {
val, extra, ok = parseString(val)
if !ok {
return nil, errShortRead
}
if len(extra) > 0 {
return nil, fmt.Errorf("ssh: unexpected trailing data after certificate option value")
}
tups[keyStr] = string(val)
} else {
tups[keyStr] = ""
}
}
return tups, nil
}
func parseCert(in []byte, privAlgo string) (*Certificate, error) {
nonce, rest, ok := parseString(in)
if !ok {
return nil, errShortRead
}
key, rest, err := parsePubKey(rest, privAlgo)
if err != nil {
return nil, err
}
var g genericCertData
if err := Unmarshal(rest, &g); err != nil {
return nil, err
}
c := &Certificate{
Nonce: nonce,
Key: key,
Serial: g.Serial,
CertType: g.CertType,
KeyId: g.KeyId,
ValidAfter: g.ValidAfter,
ValidBefore: g.ValidBefore,
}
for principals := g.ValidPrincipals; len(principals) > 0; {
principal, rest, ok := parseString(principals)
if !ok {
return nil, errShortRead
}
c.ValidPrincipals = append(c.ValidPrincipals, string(principal))
principals = rest
}
c.CriticalOptions, err = parseTuples(g.CriticalOptions)
if err != nil {
return nil, err
}
c.Extensions, err = parseTuples(g.Extensions)
if err != nil {
return nil, err
}
c.Reserved = g.Reserved
k, err := ParsePublicKey(g.SignatureKey)
if err != nil {
return nil, err
}
c.SignatureKey = k
c.Signature, rest, ok = parseSignatureBody(g.Signature)
if !ok || len(rest) > 0 {
return nil, errors.New("ssh: signature parse error")
}
return c, nil
}
type openSSHCertSigner struct {
pub *Certificate
signer Signer
}
// NewCertSigner returns a Signer that signs with the given Certificate, whose
// private key is held by signer. It returns an error if the public key in cert
// doesn't match the key used by signer.
func NewCertSigner(cert *Certificate, signer Signer) (Signer, error) {
if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 {
return nil, errors.New("ssh: signer and cert have different public key")
}
return &openSSHCertSigner{cert, signer}, nil
}
func (s *openSSHCertSigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
return s.signer.Sign(rand, data)
}
func (s *openSSHCertSigner) PublicKey() PublicKey {
return s.pub
}
const sourceAddressCriticalOption = "source-address"
// CertChecker does the work of verifying a certificate. Its methods
// can be plugged into ClientConfig.HostKeyCallback and
// ServerConfig.PublicKeyCallback. For the CertChecker to work,
// minimally, the IsAuthority callback should be set.
type CertChecker struct {
// SupportedCriticalOptions lists the CriticalOptions that the
// server application layer understands. These are only used
// for user certificates.
SupportedCriticalOptions []string
// IsAuthority should return true if the key is recognized as
// an authority. This allows for certificates to be signed by other
// certificates.
IsAuthority func(auth PublicKey) bool
// Clock is used for verifying time stamps. If nil, time.Now
// is used.
Clock func() time.Time
// UserKeyFallback is called when CertChecker.Authenticate encounters a
// public key that is not a certificate. It must implement validation
// of user keys or else, if nil, all such keys are rejected.
UserKeyFallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
// HostKeyFallback is called when CertChecker.CheckHostKey encounters a
// public key that is not a certificate. It must implement host key
// validation or else, if nil, all such keys are rejected.
HostKeyFallback func(addr string, remote net.Addr, key PublicKey) error
// IsRevoked is called for each certificate so that revocation checking
// can be implemented. It should return true if the given certificate
// is revoked and false otherwise. If nil, no certificates are
// considered to have been revoked.
IsRevoked func(cert *Certificate) bool
}
// CheckHostKey checks a host key certificate. This method can be
// plugged into ClientConfig.HostKeyCallback.
func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey) error {
cert, ok := key.(*Certificate)
if !ok {
if c.HostKeyFallback != nil {
return c.HostKeyFallback(addr, remote, key)
}
return errors.New("ssh: non-certificate host key")
}
if cert.CertType != HostCert {
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
}
return c.CheckCert(addr, cert)
}
// Authenticate checks a user certificate. Authenticate can be used as
// a value for ServerConfig.PublicKeyCallback.
func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) {
cert, ok := pubKey.(*Certificate)
if !ok {
if c.UserKeyFallback != nil {
return c.UserKeyFallback(conn, pubKey)
}
return nil, errors.New("ssh: normal key pairs not accepted")
}
if cert.CertType != UserCert {
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
}
if err := c.CheckCert(conn.User(), cert); err != nil {
return nil, err
}
return &cert.Permissions, nil
}
// CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and
// the signature of the certificate.
func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
if c.IsRevoked != nil && c.IsRevoked(cert) {
return fmt.Errorf("ssh: certicate serial %d revoked", cert.Serial)
}
for opt, _ := range cert.CriticalOptions {
// sourceAddressCriticalOption will be enforced by
// serverAuthenticate
if opt == sourceAddressCriticalOption {
continue
}
found := false
for _, supp := range c.SupportedCriticalOptions {
if supp == opt {
found = true
break
}
}
if !found {
return fmt.Errorf("ssh: unsupported critical option %q in certificate", opt)
}
}
if len(cert.ValidPrincipals) > 0 {
// By default, certs are valid for all users/hosts.
found := false
for _, p := range cert.ValidPrincipals {
if p == principal {
found = true
break
}
}
if !found {
return fmt.Errorf("ssh: principal %q not in the set of valid principals for given certificate: %q", principal, cert.ValidPrincipals)
}
}
if !c.IsAuthority(cert.SignatureKey) {
return fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
clock := c.Clock
if clock == nil {
clock = time.Now
}
unixNow := clock().Unix()
if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
return fmt.Errorf("ssh: cert is not yet valid")
}
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
return fmt.Errorf("ssh: cert has expired")
}
if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
return fmt.Errorf("ssh: certificate signature does not verify")
}
return nil
}
// SignCert sets c.SignatureKey to the authority's public key and stores a
// Signature, by authority, in the certificate.
func (c *Certificate) SignCert(rand io.Reader, authority Signer) error {
c.Nonce = make([]byte, 32)
if _, err := io.ReadFull(rand, c.Nonce); err != nil {
return err
}
c.SignatureKey = authority.PublicKey()
sig, err := authority.Sign(rand, c.bytesForSigning())
if err != nil {
return err
}
c.Signature = sig
return nil
}
var certAlgoNames = map[string]string{
KeyAlgoRSA: CertAlgoRSAv01,
KeyAlgoDSA: CertAlgoDSAv01,
KeyAlgoECDSA256: CertAlgoECDSA256v01,
KeyAlgoECDSA384: CertAlgoECDSA384v01,
KeyAlgoECDSA521: CertAlgoECDSA521v01,
}
// certToPrivAlgo returns the underlying algorithm for a certificate algorithm.
// Panics if a non-certificate algorithm is passed.
func certToPrivAlgo(algo string) string {
for privAlgo, pubAlgo := range certAlgoNames {
if pubAlgo == algo {
return privAlgo
}
}
panic("unknown cert algorithm")
}
func (cert *Certificate) bytesForSigning() []byte {
c2 := *cert
c2.Signature = nil
out := c2.Marshal()
// Drop trailing signature length.
return out[:len(out)-4]
}
// Marshal serializes c into OpenSSH's wire format. It is part of the
// PublicKey interface.
func (c *Certificate) Marshal() []byte {
generic := genericCertData{
Serial: c.Serial,
CertType: c.CertType,
KeyId: c.KeyId,
ValidPrincipals: marshalStringList(c.ValidPrincipals),
ValidAfter: uint64(c.ValidAfter),
ValidBefore: uint64(c.ValidBefore),
CriticalOptions: marshalTuples(c.CriticalOptions),
Extensions: marshalTuples(c.Extensions),
Reserved: c.Reserved,
SignatureKey: c.SignatureKey.Marshal(),
}
if c.Signature != nil {
generic.Signature = Marshal(c.Signature)
}
genericBytes := Marshal(&generic)
keyBytes := c.Key.Marshal()
_, keyBytes, _ = parseString(keyBytes)
prefix := Marshal(&struct {
Name string
Nonce []byte
Key []byte `ssh:"rest"`
}{c.Type(), c.Nonce, keyBytes})
result := make([]byte, 0, len(prefix)+len(genericBytes))
result = append(result, prefix...)
result = append(result, genericBytes...)
return result
}
// Type returns the key name. It is part of the PublicKey interface.
func (c *Certificate) Type() string {
algo, ok := certAlgoNames[c.Key.Type()]
if !ok {
panic("unknown cert key type")
}
return algo
}
// Verify verifies a signature against the certificate's public
// key. It is part of the PublicKey interface.
func (c *Certificate) Verify(data []byte, sig *Signature) error {
return c.Key.Verify(data, sig)
}
func parseSignatureBody(in []byte) (out *Signature, rest []byte, ok bool) {
format, in, ok := parseString(in)
if !ok {
return
}
out = &Signature{
Format: string(format),
}
if out.Blob, in, ok = parseString(in); !ok {
return
}
return out, in, ok
}
func parseSignature(in []byte) (out *Signature, rest []byte, ok bool) {
sigBytes, rest, ok := parseString(in)
if !ok {
return
}
out, trailing, ok := parseSignatureBody(sigBytes)
if !ok || len(trailing) > 0 {
return nil, nil, false
}
return
}

216
modules/crypto/ssh/certs_test.go Executable file
View File

@@ -0,0 +1,216 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/rand"
"reflect"
"testing"
"time"
)
// Cert generated by ssh-keygen 6.0p1 Debian-4.
// % ssh-keygen -s ca-key -I test user-key
const exampleSSHCert = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgb1srW/W3ZDjYAO45xLYAwzHBDLsJ4Ux6ICFIkTjb1LEAAAADAQABAAAAYQCkoR51poH0wE8w72cqSB8Sszx+vAhzcMdCO0wqHTj7UNENHWEXGrU0E0UQekD7U+yhkhtoyjbPOVIP7hNa6aRk/ezdh/iUnCIt4Jt1v3Z1h1P+hA4QuYFMHNB+rmjPwAcAAAAAAAAAAAAAAAEAAAAEdGVzdAAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAHcAAAAHc3NoLXJzYQAAAAMBAAEAAABhANFS2kaktpSGc+CcmEKPyw9mJC4nZKxHKTgLVZeaGbFZOvJTNzBspQHdy7Q1uKSfktxpgjZnksiu/tFF9ngyY2KFoc+U88ya95IZUycBGCUbBQ8+bhDtw/icdDGQD5WnUwAAAG8AAAAHc3NoLXJzYQAAAGC8Y9Z2LQKhIhxf52773XaWrXdxP0t3GBVo4A10vUWiYoAGepr6rQIoGGXFxT4B9Gp+nEBJjOwKDXPrAevow0T9ca8gZN+0ykbhSrXLE5Ao48rqr3zP4O1/9P7e6gp0gw8=`
func TestParseCert(t *testing.T) {
authKeyBytes := []byte(exampleSSHCert)
key, _, _, rest, err := ParseAuthorizedKey(authKeyBytes)
if err != nil {
t.Fatalf("ParseAuthorizedKey: %v", err)
}
if len(rest) > 0 {
t.Errorf("rest: got %q, want empty", rest)
}
if _, ok := key.(*Certificate); !ok {
t.Fatalf("got %v (%T), want *Certificate", key, key)
}
marshaled := MarshalAuthorizedKey(key)
// Before comparison, remove the trailing newline that
// MarshalAuthorizedKey adds.
marshaled = marshaled[:len(marshaled)-1]
if !bytes.Equal(authKeyBytes, marshaled) {
t.Errorf("marshaled certificate does not match original: got %q, want %q", marshaled, authKeyBytes)
}
}
// Cert generated by ssh-keygen OpenSSH_6.8p1 OS X 10.10.3
// % ssh-keygen -s ca -I testcert -O source-address=192.168.1.0/24 -O force-command=/bin/sleep user.pub
// user.pub key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMN
// Critical Options:
// force-command /bin/sleep
// source-address 192.168.1.0/24
// Extensions:
// permit-X11-forwarding
// permit-agent-forwarding
// permit-port-forwarding
// permit-pty
// permit-user-rc
const exampleSSHCertWithOptions = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgDyysCJY0XrO1n03EeRRoITnTPdjENFmWDs9X58PP3VUAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMNAAAAAAAAAAAAAAABAAAACHRlc3RjZXJ0AAAAAAAAAAAAAAAA//////////8AAABLAAAADWZvcmNlLWNvbW1hbmQAAAAOAAAACi9iaW4vc2xlZXAAAAAOc291cmNlLWFkZHJlc3MAAAASAAAADjE5Mi4xNjguMS4wLzI0AAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEAwU+c5ui5A8+J/CFpjW8wCa52bEODA808WWQDCSuTG/eMXNf59v9Y8Pk0F1E9dGCosSNyVcB/hacUrc6He+i97+HJCyKavBsE6GDxrjRyxYqAlfcOXi/IVmaUGiO8OQ39d4GHrjToInKvExSUeleQyH4Y4/e27T/pILAqPFL3fyrvMLT5qU9QyIt6zIpa7GBP5+urouNavMprV3zsfIqNBbWypinOQAw823a5wN+zwXnhZrgQiHZ/USG09Y6k98y1dTVz8YHlQVR4D3lpTAsKDKJ5hCH9WU4fdf+lU8OyNGaJ/vz0XNqxcToe1l4numLTnaoSuH89pHryjqurB7lJKwAAAQ8AAAAHc3NoLXJzYQAAAQCaHvUIoPL1zWUHIXLvu96/HU1s/i4CAW2IIEuGgxCUCiFj6vyTyYtgxQxcmbfZf6eaITlS6XJZa7Qq4iaFZh75C1DXTX8labXhRSD4E2t//AIP9MC1rtQC5xo6FmbQ+BoKcDskr+mNACcbRSxs3IL3bwCfWDnIw2WbVox9ZdcthJKk4UoCW4ix4QwdHw7zlddlz++fGEEVhmTbll1SUkycGApPFBsAYRTMupUJcYPIeReBI/m8XfkoMk99bV8ZJQTAd7OekHY2/48Ff53jLmyDjP7kNw1F8OaPtkFs6dGJXta4krmaekPy87j+35In5hFj7yoOqvSbmYUkeX70/GGQ`
func TestParseCertWithOptions(t *testing.T) {
opts := map[string]string{
"source-address": "192.168.1.0/24",
"force-command": "/bin/sleep",
}
exts := map[string]string{
"permit-X11-forwarding": "",
"permit-agent-forwarding": "",
"permit-port-forwarding": "",
"permit-pty": "",
"permit-user-rc": "",
}
authKeyBytes := []byte(exampleSSHCertWithOptions)
key, _, _, rest, err := ParseAuthorizedKey(authKeyBytes)
if err != nil {
t.Fatalf("ParseAuthorizedKey: %v", err)
}
if len(rest) > 0 {
t.Errorf("rest: got %q, want empty", rest)
}
cert, ok := key.(*Certificate)
if !ok {
t.Fatalf("got %v (%T), want *Certificate", key, key)
}
if !reflect.DeepEqual(cert.CriticalOptions, opts) {
t.Errorf("unexpected critical options - got %v, want %v", cert.CriticalOptions, opts)
}
if !reflect.DeepEqual(cert.Extensions, exts) {
t.Errorf("unexpected Extensions - got %v, want %v", cert.Extensions, exts)
}
marshaled := MarshalAuthorizedKey(key)
// Before comparison, remove the trailing newline that
// MarshalAuthorizedKey adds.
marshaled = marshaled[:len(marshaled)-1]
if !bytes.Equal(authKeyBytes, marshaled) {
t.Errorf("marshaled certificate does not match original: got %q, want %q", marshaled, authKeyBytes)
}
}
func TestValidateCert(t *testing.T) {
key, _, _, _, err := ParseAuthorizedKey([]byte(exampleSSHCert))
if err != nil {
t.Fatalf("ParseAuthorizedKey: %v", err)
}
validCert, ok := key.(*Certificate)
if !ok {
t.Fatalf("got %v (%T), want *Certificate", key, key)
}
checker := CertChecker{}
checker.IsAuthority = func(k PublicKey) bool {
return bytes.Equal(k.Marshal(), validCert.SignatureKey.Marshal())
}
if err := checker.CheckCert("user", validCert); err != nil {
t.Errorf("Unable to validate certificate: %v", err)
}
invalidCert := &Certificate{
Key: testPublicKeys["rsa"],
SignatureKey: testPublicKeys["ecdsa"],
ValidBefore: CertTimeInfinity,
Signature: &Signature{},
}
if err := checker.CheckCert("user", invalidCert); err == nil {
t.Error("Invalid cert signature passed validation")
}
}
func TestValidateCertTime(t *testing.T) {
cert := Certificate{
ValidPrincipals: []string{"user"},
Key: testPublicKeys["rsa"],
ValidAfter: 50,
ValidBefore: 100,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
for ts, ok := range map[int64]bool{
25: false,
50: true,
99: true,
100: false,
125: false,
} {
checker := CertChecker{
Clock: func() time.Time { return time.Unix(ts, 0) },
}
checker.IsAuthority = func(k PublicKey) bool {
return bytes.Equal(k.Marshal(),
testPublicKeys["ecdsa"].Marshal())
}
if v := checker.CheckCert("user", &cert); (v == nil) != ok {
t.Errorf("Authenticate(%d): %v", ts, v)
}
}
}
// TODO(hanwen): tests for
//
// host keys:
// * fallbacks
func TestHostKeyCert(t *testing.T) {
cert := &Certificate{
ValidPrincipals: []string{"hostname", "hostname.domain"},
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: HostCert,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
checker := &CertChecker{
IsAuthority: func(p PublicKey) bool {
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
},
}
certSigner, err := NewCertSigner(cert, testSigners["rsa"])
if err != nil {
t.Errorf("NewCertSigner: %v", err)
}
for _, name := range []string{"hostname", "otherhost"} {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
errc := make(chan error)
go func() {
conf := ServerConfig{
NoClientAuth: true,
}
conf.AddHostKey(certSigner)
_, _, _, err := NewServerConn(c1, &conf)
errc <- err
}()
config := &ClientConfig{
User: "user",
HostKeyCallback: checker.CheckHostKey,
}
_, _, _, err = NewClientConn(c2, name, config)
succeed := name == "hostname"
if (err == nil) != succeed {
t.Fatalf("NewClientConn(%q): %v", name, err)
}
err = <-errc
if (err == nil) != succeed {
t.Fatalf("NewServerConn(%q): %v", name, err)
}
}
}

631
modules/crypto/ssh/channel.go Executable file
View File

@@ -0,0 +1,631 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"sync"
)
const (
minPacketLength = 9
// channelMaxPacket contains the maximum number of bytes that will be
// sent in a single packet. As per RFC 4253, section 6.1, 32k is also
// the minimum.
channelMaxPacket = 1 << 15
// We follow OpenSSH here.
channelWindowSize = 64 * channelMaxPacket
)
// NewChannel represents an incoming request to a channel. It must either be
// accepted for use by calling Accept, or rejected by calling Reject.
type NewChannel interface {
// Accept accepts the channel creation request. It returns the Channel
// and a Go channel containing SSH requests. The Go channel must be
// serviced otherwise the Channel will hang.
Accept() (Channel, <-chan *Request, error)
// Reject rejects the channel creation request. After calling
// this, no other methods on the Channel may be called.
Reject(reason RejectionReason, message string) error
// ChannelType returns the type of the channel, as supplied by the
// client.
ChannelType() string
// ExtraData returns the arbitrary payload for this channel, as supplied
// by the client. This data is specific to the channel type.
ExtraData() []byte
}
// A Channel is an ordered, reliable, flow-controlled, duplex stream
// that is multiplexed over an SSH connection.
type Channel interface {
// Read reads up to len(data) bytes from the channel.
Read(data []byte) (int, error)
// Write writes len(data) bytes to the channel.
Write(data []byte) (int, error)
// Close signals end of channel use. No data may be sent after this
// call.
Close() error
// CloseWrite signals the end of sending in-band
// data. Requests may still be sent, and the other side may
// still send data
CloseWrite() error
// SendRequest sends a channel request. If wantReply is true,
// it will wait for a reply and return the result as a
// boolean, otherwise the return value will be false. Channel
// requests are out-of-band messages so they may be sent even
// if the data stream is closed or blocked by flow control.
SendRequest(name string, wantReply bool, payload []byte) (bool, error)
// Stderr returns an io.ReadWriter that writes to this channel
// with the extended data type set to stderr. Stderr may
// safely be read and written from a different goroutine than
// Read and Write respectively.
Stderr() io.ReadWriter
}
// Request is a request sent outside of the normal stream of
// data. Requests can either be specific to an SSH channel, or they
// can be global.
type Request struct {
Type string
WantReply bool
Payload []byte
ch *channel
mux *mux
}
// Reply sends a response to a request. It must be called for all requests
// where WantReply is true and is a no-op otherwise. The payload argument is
// ignored for replies to channel-specific requests.
func (r *Request) Reply(ok bool, payload []byte) error {
if !r.WantReply {
return nil
}
if r.ch == nil {
return r.mux.ackRequest(ok, payload)
}
return r.ch.ackRequest(ok)
}
// RejectionReason is an enumeration used when rejecting channel creation
// requests. See RFC 4254, section 5.1.
type RejectionReason uint32
const (
Prohibited RejectionReason = iota + 1
ConnectionFailed
UnknownChannelType
ResourceShortage
)
// String converts the rejection reason to human readable form.
func (r RejectionReason) String() string {
switch r {
case Prohibited:
return "administratively prohibited"
case ConnectionFailed:
return "connect failed"
case UnknownChannelType:
return "unknown channel type"
case ResourceShortage:
return "resource shortage"
}
return fmt.Sprintf("unknown reason %d", int(r))
}
func min(a uint32, b int) uint32 {
if a < uint32(b) {
return a
}
return uint32(b)
}
type channelDirection uint8
const (
channelInbound channelDirection = iota
channelOutbound
)
// channel is an implementation of the Channel interface that works
// with the mux class.
type channel struct {
// R/O after creation
chanType string
extraData []byte
localId, remoteId uint32
// maxIncomingPayload and maxRemotePayload are the maximum
// payload sizes of normal and extended data packets for
// receiving and sending, respectively. The wire packet will
// be 9 or 13 bytes larger (excluding encryption overhead).
maxIncomingPayload uint32
maxRemotePayload uint32
mux *mux
// decided is set to true if an accept or reject message has been sent
// (for outbound channels) or received (for inbound channels).
decided bool
// direction contains either channelOutbound, for channels created
// locally, or channelInbound, for channels created by the peer.
direction channelDirection
// Pending internal channel messages.
msg chan interface{}
// Since requests have no ID, there can be only one request
// with WantReply=true outstanding. This lock is held by a
// goroutine that has such an outgoing request pending.
sentRequestMu sync.Mutex
incomingRequests chan *Request
sentEOF bool
// thread-safe data
remoteWin window
pending *buffer
extPending *buffer
// windowMu protects myWindow, the flow-control window.
windowMu sync.Mutex
myWindow uint32
// writeMu serializes calls to mux.conn.writePacket() and
// protects sentClose and packetPool. This mutex must be
// different from windowMu, as writePacket can block if there
// is a key exchange pending.
writeMu sync.Mutex
sentClose bool
// packetPool has a buffer for each extended channel ID to
// save allocations during writes.
packetPool map[uint32][]byte
}
// writePacket sends a packet. If the packet is a channel close, it updates
// sentClose. This method takes the lock c.writeMu.
func (c *channel) writePacket(packet []byte) error {
c.writeMu.Lock()
if c.sentClose {
c.writeMu.Unlock()
return io.EOF
}
c.sentClose = (packet[0] == msgChannelClose)
err := c.mux.conn.writePacket(packet)
c.writeMu.Unlock()
return err
}
func (c *channel) sendMessage(msg interface{}) error {
if debugMux {
log.Printf("send %d: %#v", c.mux.chanList.offset, msg)
}
p := Marshal(msg)
binary.BigEndian.PutUint32(p[1:], c.remoteId)
return c.writePacket(p)
}
// WriteExtended writes data to a specific extended stream. These streams are
// used, for example, for stderr.
func (c *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err error) {
if c.sentEOF {
return 0, io.EOF
}
// 1 byte message type, 4 bytes remoteId, 4 bytes data length
opCode := byte(msgChannelData)
headerLength := uint32(9)
if extendedCode > 0 {
headerLength += 4
opCode = msgChannelExtendedData
}
c.writeMu.Lock()
packet := c.packetPool[extendedCode]
// We don't remove the buffer from packetPool, so
// WriteExtended calls from different goroutines will be
// flagged as errors by the race detector.
c.writeMu.Unlock()
for len(data) > 0 {
space := min(c.maxRemotePayload, len(data))
if space, err = c.remoteWin.reserve(space); err != nil {
return n, err
}
if want := headerLength + space; uint32(cap(packet)) < want {
packet = make([]byte, want)
} else {
packet = packet[:want]
}
todo := data[:space]
packet[0] = opCode
binary.BigEndian.PutUint32(packet[1:], c.remoteId)
if extendedCode > 0 {
binary.BigEndian.PutUint32(packet[5:], uint32(extendedCode))
}
binary.BigEndian.PutUint32(packet[headerLength-4:], uint32(len(todo)))
copy(packet[headerLength:], todo)
if err = c.writePacket(packet); err != nil {
return n, err
}
n += len(todo)
data = data[len(todo):]
}
c.writeMu.Lock()
c.packetPool[extendedCode] = packet
c.writeMu.Unlock()
return n, err
}
func (c *channel) handleData(packet []byte) error {
headerLen := 9
isExtendedData := packet[0] == msgChannelExtendedData
if isExtendedData {
headerLen = 13
}
if len(packet) < headerLen {
// malformed data packet
return parseError(packet[0])
}
var extended uint32
if isExtendedData {
extended = binary.BigEndian.Uint32(packet[5:])
}
length := binary.BigEndian.Uint32(packet[headerLen-4 : headerLen])
if length == 0 {
return nil
}
if length > c.maxIncomingPayload {
// TODO(hanwen): should send Disconnect?
return errors.New("ssh: incoming packet exceeds maximum payload size")
}
data := packet[headerLen:]
if length != uint32(len(data)) {
return errors.New("ssh: wrong packet length")
}
c.windowMu.Lock()
if c.myWindow < length {
c.windowMu.Unlock()
// TODO(hanwen): should send Disconnect with reason?
return errors.New("ssh: remote side wrote too much")
}
c.myWindow -= length
c.windowMu.Unlock()
if extended == 1 {
c.extPending.write(data)
} else if extended > 0 {
// discard other extended data.
} else {
c.pending.write(data)
}
return nil
}
func (c *channel) adjustWindow(n uint32) error {
c.windowMu.Lock()
// Since myWindow is managed on our side, and can never exceed
// the initial window setting, we don't worry about overflow.
c.myWindow += uint32(n)
c.windowMu.Unlock()
return c.sendMessage(windowAdjustMsg{
AdditionalBytes: uint32(n),
})
}
func (c *channel) ReadExtended(data []byte, extended uint32) (n int, err error) {
switch extended {
case 1:
n, err = c.extPending.Read(data)
case 0:
n, err = c.pending.Read(data)
default:
return 0, fmt.Errorf("ssh: extended code %d unimplemented", extended)
}
if n > 0 {
err = c.adjustWindow(uint32(n))
// sendWindowAdjust can return io.EOF if the remote
// peer has closed the connection, however we want to
// defer forwarding io.EOF to the caller of Read until
// the buffer has been drained.
if n > 0 && err == io.EOF {
err = nil
}
}
return n, err
}
func (c *channel) close() {
c.pending.eof()
c.extPending.eof()
close(c.msg)
close(c.incomingRequests)
c.writeMu.Lock()
// This is not necesary for a normal channel teardown, but if
// there was another error, it is.
c.sentClose = true
c.writeMu.Unlock()
// Unblock writers.
c.remoteWin.close()
}
// responseMessageReceived is called when a success or failure message is
// received on a channel to check that such a message is reasonable for the
// given channel.
func (c *channel) responseMessageReceived() error {
if c.direction == channelInbound {
return errors.New("ssh: channel response message received on inbound channel")
}
if c.decided {
return errors.New("ssh: duplicate response received for channel")
}
c.decided = true
return nil
}
func (c *channel) handlePacket(packet []byte) error {
switch packet[0] {
case msgChannelData, msgChannelExtendedData:
return c.handleData(packet)
case msgChannelClose:
c.sendMessage(channelCloseMsg{PeersId: c.remoteId})
c.mux.chanList.remove(c.localId)
c.close()
return nil
case msgChannelEOF:
// RFC 4254 is mute on how EOF affects dataExt messages but
// it is logical to signal EOF at the same time.
c.extPending.eof()
c.pending.eof()
return nil
}
decoded, err := decode(packet)
if err != nil {
return err
}
switch msg := decoded.(type) {
case *channelOpenFailureMsg:
if err := c.responseMessageReceived(); err != nil {
return err
}
c.mux.chanList.remove(msg.PeersId)
c.msg <- msg
case *channelOpenConfirmMsg:
if err := c.responseMessageReceived(); err != nil {
return err
}
if msg.MaxPacketSize < minPacketLength || msg.MaxPacketSize > 1<<31 {
return fmt.Errorf("ssh: invalid MaxPacketSize %d from peer", msg.MaxPacketSize)
}
c.remoteId = msg.MyId
c.maxRemotePayload = msg.MaxPacketSize
c.remoteWin.add(msg.MyWindow)
c.msg <- msg
case *windowAdjustMsg:
if !c.remoteWin.add(msg.AdditionalBytes) {
return fmt.Errorf("ssh: invalid window update for %d bytes", msg.AdditionalBytes)
}
case *channelRequestMsg:
req := Request{
Type: msg.Request,
WantReply: msg.WantReply,
Payload: msg.RequestSpecificData,
ch: c,
}
c.incomingRequests <- &req
default:
c.msg <- msg
}
return nil
}
func (m *mux) newChannel(chanType string, direction channelDirection, extraData []byte) *channel {
ch := &channel{
remoteWin: window{Cond: newCond()},
myWindow: channelWindowSize,
pending: newBuffer(),
extPending: newBuffer(),
direction: direction,
incomingRequests: make(chan *Request, 16),
msg: make(chan interface{}, 16),
chanType: chanType,
extraData: extraData,
mux: m,
packetPool: make(map[uint32][]byte),
}
ch.localId = m.chanList.add(ch)
return ch
}
var errUndecided = errors.New("ssh: must Accept or Reject channel")
var errDecidedAlready = errors.New("ssh: can call Accept or Reject only once")
type extChannel struct {
code uint32
ch *channel
}
func (e *extChannel) Write(data []byte) (n int, err error) {
return e.ch.WriteExtended(data, e.code)
}
func (e *extChannel) Read(data []byte) (n int, err error) {
return e.ch.ReadExtended(data, e.code)
}
func (c *channel) Accept() (Channel, <-chan *Request, error) {
if c.decided {
return nil, nil, errDecidedAlready
}
c.maxIncomingPayload = channelMaxPacket
confirm := channelOpenConfirmMsg{
PeersId: c.remoteId,
MyId: c.localId,
MyWindow: c.myWindow,
MaxPacketSize: c.maxIncomingPayload,
}
c.decided = true
if err := c.sendMessage(confirm); err != nil {
return nil, nil, err
}
return c, c.incomingRequests, nil
}
func (ch *channel) Reject(reason RejectionReason, message string) error {
if ch.decided {
return errDecidedAlready
}
reject := channelOpenFailureMsg{
PeersId: ch.remoteId,
Reason: reason,
Message: message,
Language: "en",
}
ch.decided = true
return ch.sendMessage(reject)
}
func (ch *channel) Read(data []byte) (int, error) {
if !ch.decided {
return 0, errUndecided
}
return ch.ReadExtended(data, 0)
}
func (ch *channel) Write(data []byte) (int, error) {
if !ch.decided {
return 0, errUndecided
}
return ch.WriteExtended(data, 0)
}
func (ch *channel) CloseWrite() error {
if !ch.decided {
return errUndecided
}
ch.sentEOF = true
return ch.sendMessage(channelEOFMsg{
PeersId: ch.remoteId})
}
func (ch *channel) Close() error {
if !ch.decided {
return errUndecided
}
return ch.sendMessage(channelCloseMsg{
PeersId: ch.remoteId})
}
// Extended returns an io.ReadWriter that sends and receives data on the given,
// SSH extended stream. Such streams are used, for example, for stderr.
func (ch *channel) Extended(code uint32) io.ReadWriter {
if !ch.decided {
return nil
}
return &extChannel{code, ch}
}
func (ch *channel) Stderr() io.ReadWriter {
return ch.Extended(1)
}
func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (bool, error) {
if !ch.decided {
return false, errUndecided
}
if wantReply {
ch.sentRequestMu.Lock()
defer ch.sentRequestMu.Unlock()
}
msg := channelRequestMsg{
PeersId: ch.remoteId,
Request: name,
WantReply: wantReply,
RequestSpecificData: payload,
}
if err := ch.sendMessage(msg); err != nil {
return false, err
}
if wantReply {
m, ok := (<-ch.msg)
if !ok {
return false, io.EOF
}
switch m.(type) {
case *channelRequestFailureMsg:
return false, nil
case *channelRequestSuccessMsg:
return true, nil
default:
return false, fmt.Errorf("ssh: unexpected response to channel request: %#v", m)
}
}
return false, nil
}
// ackRequest either sends an ack or nack to the channel request.
func (ch *channel) ackRequest(ok bool) error {
if !ch.decided {
return errUndecided
}
var msg interface{}
if !ok {
msg = channelRequestFailureMsg{
PeersId: ch.remoteId,
}
} else {
msg = channelRequestSuccessMsg{
PeersId: ch.remoteId,
}
}
return ch.sendMessage(msg)
}
func (ch *channel) ChannelType() string {
return ch.chanType
}
func (ch *channel) ExtraData() []byte {
return ch.extraData
}

549
modules/crypto/ssh/cipher.go Executable file
View File

@@ -0,0 +1,549 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto/aes"
"crypto/cipher"
"crypto/rc4"
"crypto/subtle"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
"io/ioutil"
)
const (
packetSizeMultiple = 16 // TODO(huin) this should be determined by the cipher.
// RFC 4253 section 6.1 defines a minimum packet size of 32768 that implementations
// MUST be able to process (plus a few more kilobytes for padding and mac). The RFC
// indicates implementations SHOULD be able to handle larger packet sizes, but then
// waffles on about reasonable limits.
//
// OpenSSH caps their maxPacket at 256kB so we choose to do
// the same. maxPacket is also used to ensure that uint32
// length fields do not overflow, so it should remain well
// below 4G.
maxPacket = 256 * 1024
)
// noneCipher implements cipher.Stream and provides no encryption. It is used
// by the transport before the first key-exchange.
type noneCipher struct{}
func (c noneCipher) XORKeyStream(dst, src []byte) {
copy(dst, src)
}
func newAESCTR(key, iv []byte) (cipher.Stream, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewCTR(c, iv), nil
}
func newRC4(key, iv []byte) (cipher.Stream, error) {
return rc4.NewCipher(key)
}
type streamCipherMode struct {
keySize int
ivSize int
skip int
createFunc func(key, iv []byte) (cipher.Stream, error)
}
func (c *streamCipherMode) createStream(key, iv []byte) (cipher.Stream, error) {
if len(key) < c.keySize {
panic("ssh: key length too small for cipher")
}
if len(iv) < c.ivSize {
panic("ssh: iv too small for cipher")
}
stream, err := c.createFunc(key[:c.keySize], iv[:c.ivSize])
if err != nil {
return nil, err
}
var streamDump []byte
if c.skip > 0 {
streamDump = make([]byte, 512)
}
for remainingToDump := c.skip; remainingToDump > 0; {
dumpThisTime := remainingToDump
if dumpThisTime > len(streamDump) {
dumpThisTime = len(streamDump)
}
stream.XORKeyStream(streamDump[:dumpThisTime], streamDump[:dumpThisTime])
remainingToDump -= dumpThisTime
}
return stream, nil
}
// cipherModes documents properties of supported ciphers. Ciphers not included
// are not supported and will not be negotiated, even if explicitly requested in
// ClientConfig.Crypto.Ciphers.
var cipherModes = map[string]*streamCipherMode{
// Ciphers from RFC4344, which introduced many CTR-based ciphers. Algorithms
// are defined in the order specified in the RFC.
"aes128-ctr": {16, aes.BlockSize, 0, newAESCTR},
"aes192-ctr": {24, aes.BlockSize, 0, newAESCTR},
"aes256-ctr": {32, aes.BlockSize, 0, newAESCTR},
// Ciphers from RFC4345, which introduces security-improved arcfour ciphers.
// They are defined in the order specified in the RFC.
"arcfour128": {16, 0, 1536, newRC4},
"arcfour256": {32, 0, 1536, newRC4},
// Cipher defined in RFC 4253, which describes SSH Transport Layer Protocol.
// Note that this cipher is not safe, as stated in RFC 4253: "Arcfour (and
// RC4) has problems with weak keys, and should be used with caution."
// RFC4345 introduces improved versions of Arcfour.
"arcfour": {16, 0, 0, newRC4},
// AES-GCM is not a stream cipher, so it is constructed with a
// special case. If we add any more non-stream ciphers, we
// should invest a cleaner way to do this.
gcmCipherID: {16, 12, 0, nil},
// insecure cipher, see http://www.isg.rhul.ac.uk/~kp/SandPfinal.pdf
// uncomment below to enable it.
// aes128cbcID: {16, aes.BlockSize, 0, nil},
}
// prefixLen is the length of the packet prefix that contains the packet length
// and number of padding bytes.
const prefixLen = 5
// streamPacketCipher is a packetCipher using a stream cipher.
type streamPacketCipher struct {
mac hash.Hash
cipher cipher.Stream
// The following members are to avoid per-packet allocations.
prefix [prefixLen]byte
seqNumBytes [4]byte
padding [2 * packetSizeMultiple]byte
packetData []byte
macResult []byte
}
// readPacket reads and decrypt a single packet from the reader argument.
func (s *streamPacketCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
if _, err := io.ReadFull(r, s.prefix[:]); err != nil {
return nil, err
}
s.cipher.XORKeyStream(s.prefix[:], s.prefix[:])
length := binary.BigEndian.Uint32(s.prefix[0:4])
paddingLength := uint32(s.prefix[4])
var macSize uint32
if s.mac != nil {
s.mac.Reset()
binary.BigEndian.PutUint32(s.seqNumBytes[:], seqNum)
s.mac.Write(s.seqNumBytes[:])
s.mac.Write(s.prefix[:])
macSize = uint32(s.mac.Size())
}
if length <= paddingLength+1 {
return nil, errors.New("ssh: invalid packet length, packet too small")
}
if length > maxPacket {
return nil, errors.New("ssh: invalid packet length, packet too large")
}
// the maxPacket check above ensures that length-1+macSize
// does not overflow.
if uint32(cap(s.packetData)) < length-1+macSize {
s.packetData = make([]byte, length-1+macSize)
} else {
s.packetData = s.packetData[:length-1+macSize]
}
if _, err := io.ReadFull(r, s.packetData); err != nil {
return nil, err
}
mac := s.packetData[length-1:]
data := s.packetData[:length-1]
s.cipher.XORKeyStream(data, data)
if s.mac != nil {
s.mac.Write(data)
s.macResult = s.mac.Sum(s.macResult[:0])
if subtle.ConstantTimeCompare(s.macResult, mac) != 1 {
return nil, errors.New("ssh: MAC failure")
}
}
return s.packetData[:length-paddingLength-1], nil
}
// writePacket encrypts and sends a packet of data to the writer argument
func (s *streamPacketCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error {
if len(packet) > maxPacket {
return errors.New("ssh: packet too large")
}
paddingLength := packetSizeMultiple - (prefixLen+len(packet))%packetSizeMultiple
if paddingLength < 4 {
paddingLength += packetSizeMultiple
}
length := len(packet) + 1 + paddingLength
binary.BigEndian.PutUint32(s.prefix[:], uint32(length))
s.prefix[4] = byte(paddingLength)
padding := s.padding[:paddingLength]
if _, err := io.ReadFull(rand, padding); err != nil {
return err
}
if s.mac != nil {
s.mac.Reset()
binary.BigEndian.PutUint32(s.seqNumBytes[:], seqNum)
s.mac.Write(s.seqNumBytes[:])
s.mac.Write(s.prefix[:])
s.mac.Write(packet)
s.mac.Write(padding)
}
s.cipher.XORKeyStream(s.prefix[:], s.prefix[:])
s.cipher.XORKeyStream(packet, packet)
s.cipher.XORKeyStream(padding, padding)
if _, err := w.Write(s.prefix[:]); err != nil {
return err
}
if _, err := w.Write(packet); err != nil {
return err
}
if _, err := w.Write(padding); err != nil {
return err
}
if s.mac != nil {
s.macResult = s.mac.Sum(s.macResult[:0])
if _, err := w.Write(s.macResult); err != nil {
return err
}
}
return nil
}
type gcmCipher struct {
aead cipher.AEAD
prefix [4]byte
iv []byte
buf []byte
}
func newGCMCipher(iv, key, macKey []byte) (packetCipher, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
return &gcmCipher{
aead: aead,
iv: iv,
}, nil
}
const gcmTagSize = 16
func (c *gcmCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error {
// Pad out to multiple of 16 bytes. This is different from the
// stream cipher because that encrypts the length too.
padding := byte(packetSizeMultiple - (1+len(packet))%packetSizeMultiple)
if padding < 4 {
padding += packetSizeMultiple
}
length := uint32(len(packet) + int(padding) + 1)
binary.BigEndian.PutUint32(c.prefix[:], length)
if _, err := w.Write(c.prefix[:]); err != nil {
return err
}
if cap(c.buf) < int(length) {
c.buf = make([]byte, length)
} else {
c.buf = c.buf[:length]
}
c.buf[0] = padding
copy(c.buf[1:], packet)
if _, err := io.ReadFull(rand, c.buf[1+len(packet):]); err != nil {
return err
}
c.buf = c.aead.Seal(c.buf[:0], c.iv, c.buf, c.prefix[:])
if _, err := w.Write(c.buf); err != nil {
return err
}
c.incIV()
return nil
}
func (c *gcmCipher) incIV() {
for i := 4 + 7; i >= 4; i-- {
c.iv[i]++
if c.iv[i] != 0 {
break
}
}
}
func (c *gcmCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
if _, err := io.ReadFull(r, c.prefix[:]); err != nil {
return nil, err
}
length := binary.BigEndian.Uint32(c.prefix[:])
if length > maxPacket {
return nil, errors.New("ssh: max packet length exceeded.")
}
if cap(c.buf) < int(length+gcmTagSize) {
c.buf = make([]byte, length+gcmTagSize)
} else {
c.buf = c.buf[:length+gcmTagSize]
}
if _, err := io.ReadFull(r, c.buf); err != nil {
return nil, err
}
plain, err := c.aead.Open(c.buf[:0], c.iv, c.buf, c.prefix[:])
if err != nil {
return nil, err
}
c.incIV()
padding := plain[0]
if padding < 4 || padding >= 20 {
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
}
if int(padding+1) >= len(plain) {
return nil, fmt.Errorf("ssh: padding %d too large", padding)
}
plain = plain[1 : length-uint32(padding)]
return plain, nil
}
// cbcCipher implements aes128-cbc cipher defined in RFC 4253 section 6.1
type cbcCipher struct {
mac hash.Hash
macSize uint32
decrypter cipher.BlockMode
encrypter cipher.BlockMode
// The following members are to avoid per-packet allocations.
seqNumBytes [4]byte
packetData []byte
macResult []byte
// Amount of data we should still read to hide which
// verification error triggered.
oracleCamouflage uint32
}
func newAESCBCCipher(iv, key, macKey []byte, algs directionAlgorithms) (packetCipher, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cbc := &cbcCipher{
mac: macModes[algs.MAC].new(macKey),
decrypter: cipher.NewCBCDecrypter(c, iv),
encrypter: cipher.NewCBCEncrypter(c, iv),
packetData: make([]byte, 1024),
}
if cbc.mac != nil {
cbc.macSize = uint32(cbc.mac.Size())
}
return cbc, nil
}
func maxUInt32(a, b int) uint32 {
if a > b {
return uint32(a)
}
return uint32(b)
}
const (
cbcMinPacketSizeMultiple = 8
cbcMinPacketSize = 16
cbcMinPaddingSize = 4
)
// cbcError represents a verification error that may leak information.
type cbcError string
func (e cbcError) Error() string { return string(e) }
func (c *cbcCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
p, err := c.readPacketLeaky(seqNum, r)
if err != nil {
if _, ok := err.(cbcError); ok {
// Verification error: read a fixed amount of
// data, to make distinguishing between
// failing MAC and failing length check more
// difficult.
io.CopyN(ioutil.Discard, r, int64(c.oracleCamouflage))
}
}
return p, err
}
func (c *cbcCipher) readPacketLeaky(seqNum uint32, r io.Reader) ([]byte, error) {
blockSize := c.decrypter.BlockSize()
// Read the header, which will include some of the subsequent data in the
// case of block ciphers - this is copied back to the payload later.
// How many bytes of payload/padding will be read with this first read.
firstBlockLength := uint32((prefixLen + blockSize - 1) / blockSize * blockSize)
firstBlock := c.packetData[:firstBlockLength]
if _, err := io.ReadFull(r, firstBlock); err != nil {
return nil, err
}
c.oracleCamouflage = maxPacket + 4 + c.macSize - firstBlockLength
c.decrypter.CryptBlocks(firstBlock, firstBlock)
length := binary.BigEndian.Uint32(firstBlock[:4])
if length > maxPacket {
return nil, cbcError("ssh: packet too large")
}
if length+4 < maxUInt32(cbcMinPacketSize, blockSize) {
// The minimum size of a packet is 16 (or the cipher block size, whichever
// is larger) bytes.
return nil, cbcError("ssh: packet too small")
}
// The length of the packet (including the length field but not the MAC) must
// be a multiple of the block size or 8, whichever is larger.
if (length+4)%maxUInt32(cbcMinPacketSizeMultiple, blockSize) != 0 {
return nil, cbcError("ssh: invalid packet length multiple")
}
paddingLength := uint32(firstBlock[4])
if paddingLength < cbcMinPaddingSize || length <= paddingLength+1 {
return nil, cbcError("ssh: invalid packet length")
}
// Positions within the c.packetData buffer:
macStart := 4 + length
paddingStart := macStart - paddingLength
// Entire packet size, starting before length, ending at end of mac.
entirePacketSize := macStart + c.macSize
// Ensure c.packetData is large enough for the entire packet data.
if uint32(cap(c.packetData)) < entirePacketSize {
// Still need to upsize and copy, but this should be rare at runtime, only
// on upsizing the packetData buffer.
c.packetData = make([]byte, entirePacketSize)
copy(c.packetData, firstBlock)
} else {
c.packetData = c.packetData[:entirePacketSize]
}
if n, err := io.ReadFull(r, c.packetData[firstBlockLength:]); err != nil {
return nil, err
} else {
c.oracleCamouflage -= uint32(n)
}
remainingCrypted := c.packetData[firstBlockLength:macStart]
c.decrypter.CryptBlocks(remainingCrypted, remainingCrypted)
mac := c.packetData[macStart:]
if c.mac != nil {
c.mac.Reset()
binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum)
c.mac.Write(c.seqNumBytes[:])
c.mac.Write(c.packetData[:macStart])
c.macResult = c.mac.Sum(c.macResult[:0])
if subtle.ConstantTimeCompare(c.macResult, mac) != 1 {
return nil, cbcError("ssh: MAC failure")
}
}
return c.packetData[prefixLen:paddingStart], nil
}
func (c *cbcCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error {
effectiveBlockSize := maxUInt32(cbcMinPacketSizeMultiple, c.encrypter.BlockSize())
// Length of encrypted portion of the packet (header, payload, padding).
// Enforce minimum padding and packet size.
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
// Enforce block size.
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
length := encLength - 4
paddingLength := int(length) - (1 + len(packet))
// Overall buffer contains: header, payload, padding, mac.
// Space for the MAC is reserved in the capacity but not the slice length.
bufferSize := encLength + c.macSize
if uint32(cap(c.packetData)) < bufferSize {
c.packetData = make([]byte, encLength, bufferSize)
} else {
c.packetData = c.packetData[:encLength]
}
p := c.packetData
// Packet header.
binary.BigEndian.PutUint32(p, length)
p = p[4:]
p[0] = byte(paddingLength)
// Payload.
p = p[1:]
copy(p, packet)
// Padding.
p = p[len(packet):]
if _, err := io.ReadFull(rand, p); err != nil {
return err
}
if c.mac != nil {
c.mac.Reset()
binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum)
c.mac.Write(c.seqNumBytes[:])
c.mac.Write(c.packetData)
// The MAC is now appended into the capacity reserved for it earlier.
c.packetData = c.mac.Sum(c.packetData)
}
c.encrypter.CryptBlocks(c.packetData[:encLength], c.packetData[:encLength])
if _, err := w.Write(c.packetData); err != nil {
return err
}
return nil
}

127
modules/crypto/ssh/cipher_test.go Executable file
View File

@@ -0,0 +1,127 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/rand"
"testing"
)
func TestDefaultCiphersExist(t *testing.T) {
for _, cipherAlgo := range supportedCiphers {
if _, ok := cipherModes[cipherAlgo]; !ok {
t.Errorf("default cipher %q is unknown", cipherAlgo)
}
}
}
func TestPacketCiphers(t *testing.T) {
// Still test aes128cbc cipher althought it's commented out.
cipherModes[aes128cbcID] = &streamCipherMode{16, aes.BlockSize, 0, nil}
defer delete(cipherModes, aes128cbcID)
for cipher := range cipherModes {
kr := &kexResult{Hash: crypto.SHA1}
algs := directionAlgorithms{
Cipher: cipher,
MAC: "hmac-sha1",
Compression: "none",
}
client, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Errorf("newPacketCipher(client, %q): %v", cipher, err)
continue
}
server, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Errorf("newPacketCipher(client, %q): %v", cipher, err)
continue
}
want := "bla bla"
input := []byte(want)
buf := &bytes.Buffer{}
if err := client.writePacket(0, buf, rand.Reader, input); err != nil {
t.Errorf("writePacket(%q): %v", cipher, err)
continue
}
packet, err := server.readPacket(0, buf)
if err != nil {
t.Errorf("readPacket(%q): %v", cipher, err)
continue
}
if string(packet) != want {
t.Errorf("roundtrip(%q): got %q, want %q", cipher, packet, want)
}
}
}
func TestCBCOracleCounterMeasure(t *testing.T) {
cipherModes[aes128cbcID] = &streamCipherMode{16, aes.BlockSize, 0, nil}
defer delete(cipherModes, aes128cbcID)
kr := &kexResult{Hash: crypto.SHA1}
algs := directionAlgorithms{
Cipher: aes128cbcID,
MAC: "hmac-sha1",
Compression: "none",
}
client, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Fatalf("newPacketCipher(client): %v", err)
}
want := "bla bla"
input := []byte(want)
buf := &bytes.Buffer{}
if err := client.writePacket(0, buf, rand.Reader, input); err != nil {
t.Errorf("writePacket: %v", err)
}
packetSize := buf.Len()
buf.Write(make([]byte, 2*maxPacket))
// We corrupt each byte, but this usually will only test the
// 'packet too large' or 'MAC failure' cases.
lastRead := -1
for i := 0; i < packetSize; i++ {
server, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Fatalf("newPacketCipher(client): %v", err)
}
fresh := &bytes.Buffer{}
fresh.Write(buf.Bytes())
fresh.Bytes()[i] ^= 0x01
before := fresh.Len()
_, err = server.readPacket(0, fresh)
if err == nil {
t.Errorf("corrupt byte %d: readPacket succeeded ", i)
continue
}
if _, ok := err.(cbcError); !ok {
t.Errorf("corrupt byte %d: got %v (%T), want cbcError", i, err, err)
continue
}
after := fresh.Len()
bytesRead := before - after
if bytesRead < maxPacket {
t.Errorf("corrupt byte %d: read %d bytes, want more than %d", i, bytesRead, maxPacket)
continue
}
if i > 0 && bytesRead != lastRead {
t.Errorf("corrupt byte %d: read %d bytes, want %d bytes read", i, bytesRead, lastRead)
}
lastRead = bytesRead
}
}

213
modules/crypto/ssh/client.go Executable file
View File

@@ -0,0 +1,213 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"errors"
"fmt"
"net"
"sync"
)
// Client implements a traditional SSH client that supports shells,
// subprocesses, port forwarding and tunneled dialing.
type Client struct {
Conn
forwards forwardList // forwarded tcpip connections from the remote side
mu sync.Mutex
channelHandlers map[string]chan NewChannel
}
// HandleChannelOpen returns a channel on which NewChannel requests
// for the given type are sent. If the type already is being handled,
// nil is returned. The channel is closed when the connection is closed.
func (c *Client) HandleChannelOpen(channelType string) <-chan NewChannel {
c.mu.Lock()
defer c.mu.Unlock()
if c.channelHandlers == nil {
// The SSH channel has been closed.
c := make(chan NewChannel)
close(c)
return c
}
ch := c.channelHandlers[channelType]
if ch != nil {
return nil
}
ch = make(chan NewChannel, 16)
c.channelHandlers[channelType] = ch
return ch
}
// NewClient creates a Client on top of the given connection.
func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
conn := &Client{
Conn: c,
channelHandlers: make(map[string]chan NewChannel, 1),
}
go conn.handleGlobalRequests(reqs)
go conn.handleChannelOpens(chans)
go func() {
conn.Wait()
conn.forwards.closeAll()
}()
go conn.forwards.handleChannels(conn.HandleChannelOpen("forwarded-tcpip"))
return conn
}
// NewClientConn establishes an authenticated SSH connection using c
// as the underlying transport. The Request and NewChannel channels
// must be serviced or the connection will hang.
func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) {
fullConf := *config
fullConf.SetDefaults()
conn := &connection{
sshConn: sshConn{conn: c},
}
if err := conn.clientHandshake(addr, &fullConf); err != nil {
c.Close()
return nil, nil, nil, fmt.Errorf("ssh: handshake failed: %v", err)
}
conn.mux = newMux(conn.transport)
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
}
// clientHandshake performs the client side key exchange. See RFC 4253 Section
// 7.
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
if config.ClientVersion != "" {
c.clientVersion = []byte(config.ClientVersion)
} else {
c.clientVersion = []byte(packageVersion)
}
var err error
c.serverVersion, err = exchangeVersions(c.sshConn.conn, c.clientVersion)
if err != nil {
return err
}
c.transport = newClientTransport(
newTransport(c.sshConn.conn, config.Rand, true /* is client */),
c.clientVersion, c.serverVersion, config, dialAddress, c.sshConn.RemoteAddr())
if err := c.transport.requestKeyChange(); err != nil {
return err
}
if packet, err := c.transport.readPacket(); err != nil {
return err
} else if packet[0] != msgNewKeys {
return unexpectedMessageError(msgNewKeys, packet[0])
}
// We just did the key change, so the session ID is established.
c.sessionID = c.transport.getSessionID()
return c.clientAuthenticate(config)
}
// verifyHostKeySignature verifies the host key obtained in the key
// exchange.
func verifyHostKeySignature(hostKey PublicKey, result *kexResult) error {
sig, rest, ok := parseSignatureBody(result.Signature)
if len(rest) > 0 || !ok {
return errors.New("ssh: signature parse error")
}
return hostKey.Verify(result.H, sig)
}
// NewSession opens a new Session for this client. (A session is a remote
// execution of a program.)
func (c *Client) NewSession() (*Session, error) {
ch, in, err := c.OpenChannel("session", nil)
if err != nil {
return nil, err
}
return newSession(ch, in)
}
func (c *Client) handleGlobalRequests(incoming <-chan *Request) {
for r := range incoming {
// This handles keepalive messages and matches
// the behaviour of OpenSSH.
r.Reply(false, nil)
}
}
// handleChannelOpens channel open messages from the remote side.
func (c *Client) handleChannelOpens(in <-chan NewChannel) {
for ch := range in {
c.mu.Lock()
handler := c.channelHandlers[ch.ChannelType()]
c.mu.Unlock()
if handler != nil {
handler <- ch
} else {
ch.Reject(UnknownChannelType, fmt.Sprintf("unknown channel type: %v", ch.ChannelType()))
}
}
c.mu.Lock()
for _, ch := range c.channelHandlers {
close(ch)
}
c.channelHandlers = nil
c.mu.Unlock()
}
// Dial starts a client connection to the given SSH server. It is a
// convenience function that connects to the given network address,
// initiates the SSH handshake, and then sets up a Client. For access
// to incoming channels and requests, use net.Dial with NewClientConn
// instead.
func Dial(network, addr string, config *ClientConfig) (*Client, error) {
conn, err := net.Dial(network, addr)
if err != nil {
return nil, err
}
c, chans, reqs, err := NewClientConn(conn, addr, config)
if err != nil {
return nil, err
}
return NewClient(c, chans, reqs), nil
}
// A ClientConfig structure is used to configure a Client. It must not be
// modified after having been passed to an SSH function.
type ClientConfig struct {
// Config contains configuration that is shared between clients and
// servers.
Config
// User contains the username to authenticate as.
User string
// Auth contains possible authentication methods to use with the
// server. Only the first instance of a particular RFC 4252 method will
// be used during authentication.
Auth []AuthMethod
// HostKeyCallback, if not nil, is called during the cryptographic
// handshake to validate the server's host key. A nil HostKeyCallback
// implies that all host keys are accepted.
HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
// ClientVersion contains the version identification string that will
// be used for the connection. If empty, a reasonable default is used.
ClientVersion string
// HostKeyAlgorithms lists the key types that the client will
// accept from the server as host key, in order of
// preference. If empty, a reasonable default is used. Any
// string returned from PublicKey.Type method may be used, or
// any of the CertAlgoXxxx and KeyAlgoXxxx constants.
HostKeyAlgorithms []string
}

441
modules/crypto/ssh/client_auth.go Executable file
View File

@@ -0,0 +1,441 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"errors"
"fmt"
"io"
)
// clientAuthenticate authenticates with the remote server. See RFC 4252.
func (c *connection) clientAuthenticate(config *ClientConfig) error {
// initiate user auth session
if err := c.transport.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth})); err != nil {
return err
}
packet, err := c.transport.readPacket()
if err != nil {
return err
}
var serviceAccept serviceAcceptMsg
if err := Unmarshal(packet, &serviceAccept); err != nil {
return err
}
// during the authentication phase the client first attempts the "none" method
// then any untried methods suggested by the server.
tried := make(map[string]bool)
var lastMethods []string
for auth := AuthMethod(new(noneAuth)); auth != nil; {
ok, methods, err := auth.auth(c.transport.getSessionID(), config.User, c.transport, config.Rand)
if err != nil {
return err
}
if ok {
// success
return nil
}
tried[auth.method()] = true
if methods == nil {
methods = lastMethods
}
lastMethods = methods
auth = nil
findNext:
for _, a := range config.Auth {
candidateMethod := a.method()
if tried[candidateMethod] {
continue
}
for _, meth := range methods {
if meth == candidateMethod {
auth = a
break findNext
}
}
}
}
return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", keys(tried))
}
func keys(m map[string]bool) []string {
s := make([]string, 0, len(m))
for key := range m {
s = append(s, key)
}
return s
}
// An AuthMethod represents an instance of an RFC 4252 authentication method.
type AuthMethod interface {
// auth authenticates user over transport t.
// Returns true if authentication is successful.
// If authentication is not successful, a []string of alternative
// method names is returned. If the slice is nil, it will be ignored
// and the previous set of possible methods will be reused.
auth(session []byte, user string, p packetConn, rand io.Reader) (bool, []string, error)
// method returns the RFC 4252 method name.
method() string
}
// "none" authentication, RFC 4252 section 5.2.
type noneAuth int
func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
if err := c.writePacket(Marshal(&userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: "none",
})); err != nil {
return false, nil, err
}
return handleAuthResponse(c)
}
func (n *noneAuth) method() string {
return "none"
}
// passwordCallback is an AuthMethod that fetches the password through
// a function call, e.g. by prompting the user.
type passwordCallback func() (password string, err error)
func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
type passwordAuthMsg struct {
User string `sshtype:"50"`
Service string
Method string
Reply bool
Password string
}
pw, err := cb()
// REVIEW NOTE: is there a need to support skipping a password attempt?
// The program may only find out that the user doesn't have a password
// when prompting.
if err != nil {
return false, nil, err
}
if err := c.writePacket(Marshal(&passwordAuthMsg{
User: user,
Service: serviceSSH,
Method: cb.method(),
Reply: false,
Password: pw,
})); err != nil {
return false, nil, err
}
return handleAuthResponse(c)
}
func (cb passwordCallback) method() string {
return "password"
}
// Password returns an AuthMethod using the given password.
func Password(secret string) AuthMethod {
return passwordCallback(func() (string, error) { return secret, nil })
}
// PasswordCallback returns an AuthMethod that uses a callback for
// fetching a password.
func PasswordCallback(prompt func() (secret string, err error)) AuthMethod {
return passwordCallback(prompt)
}
type publickeyAuthMsg struct {
User string `sshtype:"50"`
Service string
Method string
// HasSig indicates to the receiver packet that the auth request is signed and
// should be used for authentication of the request.
HasSig bool
Algoname string
PubKey []byte
// Sig is tagged with "rest" so Marshal will exclude it during
// validateKey
Sig []byte `ssh:"rest"`
}
// publicKeyCallback is an AuthMethod that uses a set of key
// pairs for authentication.
type publicKeyCallback func() ([]Signer, error)
func (cb publicKeyCallback) method() string {
return "publickey"
}
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
// Authentication is performed in two stages. The first stage sends an
// enquiry to test if each key is acceptable to the remote. The second
// stage attempts to authenticate with the valid keys obtained in the
// first stage.
signers, err := cb()
if err != nil {
return false, nil, err
}
var validKeys []Signer
for _, signer := range signers {
if ok, err := validateKey(signer.PublicKey(), user, c); ok {
validKeys = append(validKeys, signer)
} else {
if err != nil {
return false, nil, err
}
}
}
// methods that may continue if this auth is not successful.
var methods []string
for _, signer := range validKeys {
pub := signer.PublicKey()
pubKey := pub.Marshal()
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: cb.method(),
}, []byte(pub.Type()), pubKey))
if err != nil {
return false, nil, err
}
// manually wrap the serialized signature in a string
s := Marshal(sign)
sig := make([]byte, stringLength(len(s)))
marshalString(sig, s)
msg := publickeyAuthMsg{
User: user,
Service: serviceSSH,
Method: cb.method(),
HasSig: true,
Algoname: pub.Type(),
PubKey: pubKey,
Sig: sig,
}
p := Marshal(&msg)
if err := c.writePacket(p); err != nil {
return false, nil, err
}
var success bool
success, methods, err = handleAuthResponse(c)
if err != nil {
return false, nil, err
}
if success {
return success, methods, err
}
}
return false, methods, nil
}
// validateKey validates the key provided is acceptable to the server.
func validateKey(key PublicKey, user string, c packetConn) (bool, error) {
pubKey := key.Marshal()
msg := publickeyAuthMsg{
User: user,
Service: serviceSSH,
Method: "publickey",
HasSig: false,
Algoname: key.Type(),
PubKey: pubKey,
}
if err := c.writePacket(Marshal(&msg)); err != nil {
return false, err
}
return confirmKeyAck(key, c)
}
func confirmKeyAck(key PublicKey, c packetConn) (bool, error) {
pubKey := key.Marshal()
algoname := key.Type()
for {
packet, err := c.readPacket()
if err != nil {
return false, err
}
switch packet[0] {
case msgUserAuthBanner:
// TODO(gpaul): add callback to present the banner to the user
case msgUserAuthPubKeyOk:
var msg userAuthPubKeyOkMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, err
}
if msg.Algo != algoname || !bytes.Equal(msg.PubKey, pubKey) {
return false, nil
}
return true, nil
case msgUserAuthFailure:
return false, nil
default:
return false, unexpectedMessageError(msgUserAuthSuccess, packet[0])
}
}
}
// PublicKeys returns an AuthMethod that uses the given key
// pairs.
func PublicKeys(signers ...Signer) AuthMethod {
return publicKeyCallback(func() ([]Signer, error) { return signers, nil })
}
// PublicKeysCallback returns an AuthMethod that runs the given
// function to obtain a list of key pairs.
func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMethod {
return publicKeyCallback(getSigners)
}
// handleAuthResponse returns whether the preceding authentication request succeeded
// along with a list of remaining authentication methods to try next and
// an error if an unexpected response was received.
func handleAuthResponse(c packetConn) (bool, []string, error) {
for {
packet, err := c.readPacket()
if err != nil {
return false, nil, err
}
switch packet[0] {
case msgUserAuthBanner:
// TODO: add callback to present the banner to the user
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
}
return false, msg.Methods, nil
case msgUserAuthSuccess:
return true, nil, nil
case msgDisconnect:
return false, nil, io.EOF
default:
return false, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0])
}
}
}
// KeyboardInteractiveChallenge should print questions, optionally
// disabling echoing (e.g. for passwords), and return all the answers.
// Challenge may be called multiple times in a single session. After
// successful authentication, the server may send a challenge with no
// questions, for which the user and instruction messages should be
// printed. RFC 4256 section 3.3 details how the UI should behave for
// both CLI and GUI environments.
type KeyboardInteractiveChallenge func(user, instruction string, questions []string, echos []bool) (answers []string, err error)
// KeyboardInteractive returns a AuthMethod using a prompt/response
// sequence controlled by the server.
func KeyboardInteractive(challenge KeyboardInteractiveChallenge) AuthMethod {
return challenge
}
func (cb KeyboardInteractiveChallenge) method() string {
return "keyboard-interactive"
}
func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
type initiateMsg struct {
User string `sshtype:"50"`
Service string
Method string
Language string
Submethods string
}
if err := c.writePacket(Marshal(&initiateMsg{
User: user,
Service: serviceSSH,
Method: "keyboard-interactive",
})); err != nil {
return false, nil, err
}
for {
packet, err := c.readPacket()
if err != nil {
return false, nil, err
}
// like handleAuthResponse, but with less options.
switch packet[0] {
case msgUserAuthBanner:
// TODO: Print banners during userauth.
continue
case msgUserAuthInfoRequest:
// OK
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
}
return false, msg.Methods, nil
case msgUserAuthSuccess:
return true, nil, nil
default:
return false, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0])
}
var msg userAuthInfoRequestMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
}
// Manually unpack the prompt/echo pairs.
rest := msg.Prompts
var prompts []string
var echos []bool
for i := 0; i < int(msg.NumPrompts); i++ {
prompt, r, ok := parseString(rest)
if !ok || len(r) == 0 {
return false, nil, errors.New("ssh: prompt format error")
}
prompts = append(prompts, string(prompt))
echos = append(echos, r[0] != 0)
rest = r[1:]
}
if len(rest) != 0 {
return false, nil, errors.New("ssh: extra data following keyboard-interactive pairs")
}
answers, err := cb(msg.User, msg.Instruction, prompts, echos)
if err != nil {
return false, nil, err
}
if len(answers) != len(prompts) {
return false, nil, errors.New("ssh: not enough answers from keyboard-interactive callback")
}
responseLength := 1 + 4
for _, a := range answers {
responseLength += stringLength(len(a))
}
serialized := make([]byte, responseLength)
p := serialized
p[0] = msgUserAuthInfoResponse
p = p[1:]
p = marshalUint32(p, uint32(len(answers)))
for _, a := range answers {
p = marshalString(p, []byte(a))
}
if err := c.writePacket(serialized); err != nil {
return false, nil, err
}
}
}

View File

@@ -0,0 +1,393 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"strings"
"testing"
)
type keyboardInteractive map[string]string
func (cr keyboardInteractive) Challenge(user string, instruction string, questions []string, echos []bool) ([]string, error) {
var answers []string
for _, q := range questions {
answers = append(answers, cr[q])
}
return answers, nil
}
// reused internally by tests
var clientPassword = "tiger"
// tryAuth runs a handshake with a given config against an SSH server
// with config serverConfig
func tryAuth(t *testing.T, config *ClientConfig) error {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
certChecker := CertChecker{
IsAuthority: func(k PublicKey) bool {
return bytes.Equal(k.Marshal(), testPublicKeys["ecdsa"].Marshal())
},
UserKeyFallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if conn.User() == "testuser" && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, fmt.Errorf("pubkey for %q not acceptable", conn.User())
},
IsRevoked: func(c *Certificate) bool {
return c.Serial == 666
},
}
serverConfig := &ServerConfig{
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
if conn.User() == "testuser" && string(pass) == clientPassword {
return nil, nil
}
return nil, errors.New("password auth failed")
},
PublicKeyCallback: certChecker.Authenticate,
KeyboardInteractiveCallback: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (*Permissions, error) {
ans, err := challenge("user",
"instruction",
[]string{"question1", "question2"},
[]bool{true, true})
if err != nil {
return nil, err
}
ok := conn.User() == "testuser" && ans[0] == "answer1" && ans[1] == "answer2"
if ok {
challenge("user", "motd", nil, nil)
return nil, nil
}
return nil, errors.New("keyboard-interactive failed")
},
AuthLogCallback: func(conn ConnMetadata, method string, err error) {
t.Logf("user %q, method %q: %v", conn.User(), method, err)
},
}
serverConfig.AddHostKey(testSigners["rsa"])
go newServer(c1, serverConfig)
_, _, _, err = NewClientConn(c2, "", config)
return err
}
func TestClientAuthPublicKey(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodPassword(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
Password(clientPassword),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodFallback(t *testing.T) {
var passwordCalled bool
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
PasswordCallback(
func() (string, error) {
passwordCalled = true
return "WRONG", nil
}),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
if passwordCalled {
t.Errorf("password auth tried before public-key auth.")
}
}
func TestAuthMethodWrongPassword(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
Password("wrong"),
PublicKeys(testSigners["rsa"]),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodKeyboardInteractive(t *testing.T) {
answers := keyboardInteractive(map[string]string{
"question1": "answer1",
"question2": "answer2",
})
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
KeyboardInteractive(answers.Challenge),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodWrongKeyboardInteractive(t *testing.T) {
answers := keyboardInteractive(map[string]string{
"question1": "answer1",
"question2": "WRONG",
})
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
KeyboardInteractive(answers.Challenge),
},
}
if err := tryAuth(t, config); err == nil {
t.Fatalf("wrong answers should not have authenticated with KeyboardInteractive")
}
}
// the mock server will only authenticate ssh-rsa keys
func TestAuthMethodInvalidPublicKey(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["dsa"]),
},
}
if err := tryAuth(t, config); err == nil {
t.Fatalf("dsa private key should not have authenticated with rsa public key")
}
}
// the client should authenticate with the second key
func TestAuthMethodRSAandDSA(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["dsa"], testSigners["rsa"]),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("client could not authenticate with rsa key: %v", err)
}
}
func TestClientHMAC(t *testing.T) {
for _, mac := range supportedMACs {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
Config: Config{
MACs: []string{mac},
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("client could not authenticate with mac algo %s: %v", mac, err)
}
}
}
// issue 4285.
func TestClientUnsupportedCipher(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(),
},
Config: Config{
Ciphers: []string{"aes128-cbc"}, // not currently supported
},
}
if err := tryAuth(t, config); err == nil {
t.Errorf("expected no ciphers in common")
}
}
func TestClientUnsupportedKex(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(),
},
Config: Config{
KeyExchanges: []string{"diffie-hellman-group-exchange-sha256"}, // not currently supported
},
}
if err := tryAuth(t, config); err == nil || !strings.Contains(err.Error(), "common algorithm") {
t.Errorf("got %v, expected 'common algorithm'", err)
}
}
func TestClientLoginCert(t *testing.T) {
cert := &Certificate{
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: UserCert,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
certSigner, err := NewCertSigner(cert, testSigners["rsa"])
if err != nil {
t.Fatalf("NewCertSigner: %v", err)
}
clientConfig := &ClientConfig{
User: "user",
}
clientConfig.Auth = append(clientConfig.Auth, PublicKeys(certSigner))
t.Log("should succeed")
if err := tryAuth(t, clientConfig); err != nil {
t.Errorf("cert login failed: %v", err)
}
t.Log("corrupted signature")
cert.Signature.Blob[0]++
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with corrupted sig")
}
t.Log("revoked")
cert.Serial = 666
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("revoked cert login succeeded")
}
cert.Serial = 1
t.Log("sign with wrong key")
cert.SignCert(rand.Reader, testSigners["dsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with non-authoritive key")
}
t.Log("host cert")
cert.CertType = HostCert
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with wrong type")
}
cert.CertType = UserCert
t.Log("principal specified")
cert.ValidPrincipals = []string{"user"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err != nil {
t.Errorf("cert login failed: %v", err)
}
t.Log("wrong principal specified")
cert.ValidPrincipals = []string{"fred"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with wrong principal")
}
cert.ValidPrincipals = nil
t.Log("added critical option")
cert.CriticalOptions = map[string]string{"root-access": "yes"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with unrecognized critical option")
}
t.Log("allowed source address")
cert.CriticalOptions = map[string]string{"source-address": "127.0.0.42/24"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err != nil {
t.Errorf("cert login with source-address failed: %v", err)
}
t.Log("disallowed source address")
cert.CriticalOptions = map[string]string{"source-address": "127.0.0.42"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login with source-address succeeded")
}
}
func testPermissionsPassing(withPermissions bool, t *testing.T) {
serverConfig := &ServerConfig{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if conn.User() == "nopermissions" {
return nil, nil
} else {
return &Permissions{}, nil
}
},
}
serverConfig.AddHostKey(testSigners["rsa"])
clientConfig := &ClientConfig{
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
}
if withPermissions {
clientConfig.User = "permissions"
} else {
clientConfig.User = "nopermissions"
}
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
go NewClientConn(c2, "", clientConfig)
serverConn, err := newServer(c1, serverConfig)
if err != nil {
t.Fatal(err)
}
if p := serverConn.Permissions; (p != nil) != withPermissions {
t.Fatalf("withPermissions is %t, but Permissions object is %#v", withPermissions, p)
}
}
func TestPermissionsPassing(t *testing.T) {
testPermissionsPassing(true, t)
}
func TestNoPermissionsPassing(t *testing.T) {
testPermissionsPassing(false, t)
}

View File

@@ -0,0 +1,39 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"net"
"testing"
)
func testClientVersion(t *testing.T, config *ClientConfig, expected string) {
clientConn, serverConn := net.Pipe()
defer clientConn.Close()
receivedVersion := make(chan string, 1)
go func() {
version, err := readVersion(serverConn)
if err != nil {
receivedVersion <- ""
} else {
receivedVersion <- string(version)
}
serverConn.Close()
}()
NewClientConn(clientConn, "", config)
actual := <-receivedVersion
if actual != expected {
t.Fatalf("got %s; want %s", actual, expected)
}
}
func TestCustomClientVersion(t *testing.T) {
version := "Test-Client-Version-0.0"
testClientVersion(t, &ClientConfig{ClientVersion: version}, version)
}
func TestDefaultClientVersion(t *testing.T) {
testClientVersion(t, &ClientConfig{}, packageVersion)
}

354
modules/crypto/ssh/common.go Executable file
View File

@@ -0,0 +1,354 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto"
"crypto/rand"
"fmt"
"io"
"sync"
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha512"
)
// These are string constants in the SSH protocol.
const (
compressionNone = "none"
serviceUserAuth = "ssh-userauth"
serviceSSH = "ssh-connection"
)
// supportedCiphers specifies the supported ciphers in preference order.
var supportedCiphers = []string{
"aes128-ctr", "aes192-ctr", "aes256-ctr",
"aes128-gcm@openssh.com",
"arcfour256", "arcfour128",
}
// supportedKexAlgos specifies the supported key-exchange algorithms in
// preference order.
var supportedKexAlgos = []string{
kexAlgoCurve25519SHA256,
// P384 and P521 are not constant-time yet, but since we don't
// reuse ephemeral keys, using them for ECDH should be OK.
kexAlgoECDH256, kexAlgoECDH384, kexAlgoECDH521,
kexAlgoDH14SHA1, kexAlgoDH1SHA1,
}
// supportedKexAlgos specifies the supported host-key algorithms (i.e. methods
// of authenticating servers) in preference order.
var supportedHostKeyAlgos = []string{
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01,
CertAlgoECDSA384v01, CertAlgoECDSA521v01,
KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
KeyAlgoRSA, KeyAlgoDSA,
}
// supportedMACs specifies a default set of MAC algorithms in preference order.
// This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed
// because they have reached the end of their useful life.
var supportedMACs = []string{
"hmac-sha2-256", "hmac-sha1", "hmac-sha1-96",
}
var supportedCompressions = []string{compressionNone}
// hashFuncs keeps the mapping of supported algorithms to their respective
// hashes needed for signature verification.
var hashFuncs = map[string]crypto.Hash{
KeyAlgoRSA: crypto.SHA1,
KeyAlgoDSA: crypto.SHA1,
KeyAlgoECDSA256: crypto.SHA256,
KeyAlgoECDSA384: crypto.SHA384,
KeyAlgoECDSA521: crypto.SHA512,
CertAlgoRSAv01: crypto.SHA1,
CertAlgoDSAv01: crypto.SHA1,
CertAlgoECDSA256v01: crypto.SHA256,
CertAlgoECDSA384v01: crypto.SHA384,
CertAlgoECDSA521v01: crypto.SHA512,
}
// unexpectedMessageError results when the SSH message that we received didn't
// match what we wanted.
func unexpectedMessageError(expected, got uint8) error {
return fmt.Errorf("ssh: unexpected message type %d (expected %d)", got, expected)
}
// parseError results from a malformed SSH message.
func parseError(tag uint8) error {
return fmt.Errorf("ssh: parse error in message type %d", tag)
}
func findCommon(what string, client []string, server []string) (common string, err error) {
for _, c := range client {
for _, s := range server {
if c == s {
return c, nil
}
}
}
return "", fmt.Errorf("ssh: no common algorithm for %s; client offered: %v, server offered: %v", what, client, server)
}
type directionAlgorithms struct {
Cipher string
MAC string
Compression string
}
type algorithms struct {
kex string
hostKey string
w directionAlgorithms
r directionAlgorithms
}
func findAgreedAlgorithms(clientKexInit, serverKexInit *kexInitMsg) (algs *algorithms, err error) {
result := &algorithms{}
result.kex, err = findCommon("key exchange", clientKexInit.KexAlgos, serverKexInit.KexAlgos)
if err != nil {
return
}
result.hostKey, err = findCommon("host key", clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos)
if err != nil {
return
}
result.w.Cipher, err = findCommon("client to server cipher", clientKexInit.CiphersClientServer, serverKexInit.CiphersClientServer)
if err != nil {
return
}
result.r.Cipher, err = findCommon("server to client cipher", clientKexInit.CiphersServerClient, serverKexInit.CiphersServerClient)
if err != nil {
return
}
result.w.MAC, err = findCommon("client to server MAC", clientKexInit.MACsClientServer, serverKexInit.MACsClientServer)
if err != nil {
return
}
result.r.MAC, err = findCommon("server to client MAC", clientKexInit.MACsServerClient, serverKexInit.MACsServerClient)
if err != nil {
return
}
result.w.Compression, err = findCommon("client to server compression", clientKexInit.CompressionClientServer, serverKexInit.CompressionClientServer)
if err != nil {
return
}
result.r.Compression, err = findCommon("server to client compression", clientKexInit.CompressionServerClient, serverKexInit.CompressionServerClient)
if err != nil {
return
}
return result, nil
}
// If rekeythreshold is too small, we can't make any progress sending
// stuff.
const minRekeyThreshold uint64 = 256
// Config contains configuration data common to both ServerConfig and
// ClientConfig.
type Config struct {
// Rand provides the source of entropy for cryptographic
// primitives. If Rand is nil, the cryptographic random reader
// in package crypto/rand will be used.
Rand io.Reader
// The maximum number of bytes sent or received after which a
// new key is negotiated. It must be at least 256. If
// unspecified, 1 gigabyte is used.
RekeyThreshold uint64
// The allowed key exchanges algorithms. If unspecified then a
// default set of algorithms is used.
KeyExchanges []string
// The allowed cipher algorithms. If unspecified then a sensible
// default is used.
Ciphers []string
// The allowed MAC algorithms. If unspecified then a sensible default
// is used.
MACs []string
}
// SetDefaults sets sensible values for unset fields in config. This is
// exported for testing: Configs passed to SSH functions are copied and have
// default values set automatically.
func (c *Config) SetDefaults() {
if c.Rand == nil {
c.Rand = rand.Reader
}
if c.Ciphers == nil {
c.Ciphers = supportedCiphers
}
var ciphers []string
for _, c := range c.Ciphers {
if cipherModes[c] != nil {
// reject the cipher if we have no cipherModes definition
ciphers = append(ciphers, c)
}
}
c.Ciphers = ciphers
if c.KeyExchanges == nil {
c.KeyExchanges = supportedKexAlgos
}
if c.MACs == nil {
c.MACs = supportedMACs
}
if c.RekeyThreshold == 0 {
// RFC 4253, section 9 suggests rekeying after 1G.
c.RekeyThreshold = 1 << 30
}
if c.RekeyThreshold < minRekeyThreshold {
c.RekeyThreshold = minRekeyThreshold
}
}
// buildDataSignedForAuth returns the data that is signed in order to prove
// possession of a private key. See RFC 4252, section 7.
func buildDataSignedForAuth(sessionId []byte, req userAuthRequestMsg, algo, pubKey []byte) []byte {
data := struct {
Session []byte
Type byte
User string
Service string
Method string
Sign bool
Algo []byte
PubKey []byte
}{
sessionId,
msgUserAuthRequest,
req.User,
req.Service,
req.Method,
true,
algo,
pubKey,
}
return Marshal(data)
}
func appendU16(buf []byte, n uint16) []byte {
return append(buf, byte(n>>8), byte(n))
}
func appendU32(buf []byte, n uint32) []byte {
return append(buf, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
}
func appendU64(buf []byte, n uint64) []byte {
return append(buf,
byte(n>>56), byte(n>>48), byte(n>>40), byte(n>>32),
byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
}
func appendInt(buf []byte, n int) []byte {
return appendU32(buf, uint32(n))
}
func appendString(buf []byte, s string) []byte {
buf = appendU32(buf, uint32(len(s)))
buf = append(buf, s...)
return buf
}
func appendBool(buf []byte, b bool) []byte {
if b {
return append(buf, 1)
}
return append(buf, 0)
}
// newCond is a helper to hide the fact that there is no usable zero
// value for sync.Cond.
func newCond() *sync.Cond { return sync.NewCond(new(sync.Mutex)) }
// window represents the buffer available to clients
// wishing to write to a channel.
type window struct {
*sync.Cond
win uint32 // RFC 4254 5.2 says the window size can grow to 2^32-1
writeWaiters int
closed bool
}
// add adds win to the amount of window available
// for consumers.
func (w *window) add(win uint32) bool {
// a zero sized window adjust is a noop.
if win == 0 {
return true
}
w.L.Lock()
if w.win+win < win {
w.L.Unlock()
return false
}
w.win += win
// It is unusual that multiple goroutines would be attempting to reserve
// window space, but not guaranteed. Use broadcast to notify all waiters
// that additional window is available.
w.Broadcast()
w.L.Unlock()
return true
}
// close sets the window to closed, so all reservations fail
// immediately.
func (w *window) close() {
w.L.Lock()
w.closed = true
w.Broadcast()
w.L.Unlock()
}
// reserve reserves win from the available window capacity.
// If no capacity remains, reserve will block. reserve may
// return less than requested.
func (w *window) reserve(win uint32) (uint32, error) {
var err error
w.L.Lock()
w.writeWaiters++
w.Broadcast()
for w.win == 0 && !w.closed {
w.Wait()
}
w.writeWaiters--
if w.win < win {
win = w.win
}
w.win -= win
if w.closed {
err = io.EOF
}
w.L.Unlock()
return win, err
}
// waitWriterBlocked waits until some goroutine is blocked for further
// writes. It is used in tests only.
func (w *window) waitWriterBlocked() {
w.Cond.L.Lock()
for w.writeWaiters == 0 {
w.Cond.Wait()
}
w.Cond.L.Unlock()
}

144
modules/crypto/ssh/connection.go Executable file
View File

@@ -0,0 +1,144 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"fmt"
"net"
)
// OpenChannelError is returned if the other side rejects an
// OpenChannel request.
type OpenChannelError struct {
Reason RejectionReason
Message string
}
func (e *OpenChannelError) Error() string {
return fmt.Sprintf("ssh: rejected: %s (%s)", e.Reason, e.Message)
}
// ConnMetadata holds metadata for the connection.
type ConnMetadata interface {
// User returns the user ID for this connection.
// It is empty if no authentication is used.
User() string
// SessionID returns the sesson hash, also denoted by H.
SessionID() []byte
// ClientVersion returns the client's version string as hashed
// into the session ID.
ClientVersion() []byte
// ServerVersion returns the server's version string as hashed
// into the session ID.
ServerVersion() []byte
// RemoteAddr returns the remote address for this connection.
RemoteAddr() net.Addr
// LocalAddr returns the local address for this connection.
LocalAddr() net.Addr
}
// Conn represents an SSH connection for both server and client roles.
// Conn is the basis for implementing an application layer, such
// as ClientConn, which implements the traditional shell access for
// clients.
type Conn interface {
ConnMetadata
// SendRequest sends a global request, and returns the
// reply. If wantReply is true, it returns the response status
// and payload. See also RFC4254, section 4.
SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error)
// OpenChannel tries to open an channel. If the request is
// rejected, it returns *OpenChannelError. On success it returns
// the SSH Channel and a Go channel for incoming, out-of-band
// requests. The Go channel must be serviced, or the
// connection will hang.
OpenChannel(name string, data []byte) (Channel, <-chan *Request, error)
// Close closes the underlying network connection
Close() error
// Wait blocks until the connection has shut down, and returns the
// error causing the shutdown.
Wait() error
// TODO(hanwen): consider exposing:
// RequestKeyChange
// Disconnect
}
// DiscardRequests consumes and rejects all requests from the
// passed-in channel.
func DiscardRequests(in <-chan *Request) {
for req := range in {
if req.WantReply {
req.Reply(false, nil)
}
}
}
// A connection represents an incoming connection.
type connection struct {
transport *handshakeTransport
sshConn
// The connection protocol.
*mux
}
func (c *connection) Close() error {
return c.sshConn.conn.Close()
}
// sshconn provides net.Conn metadata, but disallows direct reads and
// writes.
type sshConn struct {
conn net.Conn
user string
sessionID []byte
clientVersion []byte
serverVersion []byte
}
func dup(src []byte) []byte {
dst := make([]byte, len(src))
copy(dst, src)
return dst
}
func (c *sshConn) User() string {
return c.user
}
func (c *sshConn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *sshConn) Close() error {
return c.conn.Close()
}
func (c *sshConn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *sshConn) SessionID() []byte {
return dup(c.sessionID)
}
func (c *sshConn) ClientVersion() []byte {
return dup(c.clientVersion)
}
func (c *sshConn) ServerVersion() []byte {
return dup(c.serverVersion)
}

18
modules/crypto/ssh/doc.go Executable file
View File

@@ -0,0 +1,18 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package ssh implements an SSH client and server.
SSH is a transport security protocol, an authentication protocol and a
family of application protocols. The most typical application level
protocol is a remote shell and this is specifically implemented. However,
the multiplexed nature of SSH is exposed to users that wish to support
others.
References:
[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
[SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
*/
package ssh

View File

@@ -0,0 +1,211 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh_test
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"github.com/gogits/gogs/modules/crypto/ssh"
"github.com/gogits/gogs/modules/crypto/ssh/terminal"
)
func ExampleNewServerConn() {
// An SSH server is represented by a ServerConfig, which holds
// certificate details and handles authentication of ServerConns.
config := &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
// Should use constant-time compare (or better, salt+hash) in
// a production setting.
if c.User() == "testuser" && string(pass) == "tiger" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
}
privateBytes, err := ioutil.ReadFile("id_rsa")
if err != nil {
panic("Failed to load private key")
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
panic("Failed to parse private key")
}
config.AddHostKey(private)
// Once a ServerConfig has been configured, connections can be
// accepted.
listener, err := net.Listen("tcp", "0.0.0.0:2022")
if err != nil {
panic("failed to listen for connection")
}
nConn, err := listener.Accept()
if err != nil {
panic("failed to accept incoming connection")
}
// Before use, a handshake must be performed on the incoming
// net.Conn.
_, chans, reqs, err := ssh.NewServerConn(nConn, config)
if err != nil {
panic("failed to handshake")
}
// The incoming Request channel must be serviced.
go ssh.DiscardRequests(reqs)
// Service the incoming Channel channel.
for newChannel := range chans {
// Channels have a type, depending on the application level
// protocol intended. In the case of a shell, the type is
// "session" and ServerShell may be used to present a simple
// terminal interface.
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
panic("could not accept channel.")
}
// Sessions have out-of-band requests such as "shell",
// "pty-req" and "env". Here we handle only the
// "shell" request.
go func(in <-chan *ssh.Request) {
for req := range in {
ok := false
switch req.Type {
case "shell":
ok = true
if len(req.Payload) > 0 {
// We don't accept any
// commands, only the
// default shell.
ok = false
}
}
req.Reply(ok, nil)
}
}(requests)
term := terminal.NewTerminal(channel, "> ")
go func() {
defer channel.Close()
for {
line, err := term.ReadLine()
if err != nil {
break
}
fmt.Println(line)
}
}()
}
}
func ExampleDial() {
// An SSH client is represented with a ClientConn. Currently only
// the "password" authentication method is supported.
//
// To authenticate with the remote server you must pass at least one
// implementation of AuthMethod via the Auth field in ClientConfig.
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("yourpassword"),
},
}
client, err := ssh.Dial("tcp", "yourserver.com:22", config)
if err != nil {
panic("Failed to dial: " + err.Error())
}
// Each ClientConn can support multiple interactive sessions,
// represented by a Session.
session, err := client.NewSession()
if err != nil {
panic("Failed to create session: " + err.Error())
}
defer session.Close()
// Once a Session is created, you can execute a single command on
// the remote side using the Run method.
var b bytes.Buffer
session.Stdout = &b
if err := session.Run("/usr/bin/whoami"); err != nil {
panic("Failed to run: " + err.Error())
}
fmt.Println(b.String())
}
func ExampleClient_Listen() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
}
// Dial your ssh server.
conn, err := ssh.Dial("tcp", "localhost:22", config)
if err != nil {
log.Fatalf("unable to connect: %s", err)
}
defer conn.Close()
// Request the remote side to open port 8080 on all interfaces.
l, err := conn.Listen("tcp", "0.0.0.0:8080")
if err != nil {
log.Fatalf("unable to register tcp forward: %v", err)
}
defer l.Close()
// Serve HTTP with your SSH server acting as a reverse proxy.
http.Serve(l, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintf(resp, "Hello world!\n")
}))
}
func ExampleSession_RequestPty() {
// Create client config
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
}
// Connect to ssh server
conn, err := ssh.Dial("tcp", "localhost:22", config)
if err != nil {
log.Fatalf("unable to connect: %s", err)
}
defer conn.Close()
// Create a session
session, err := conn.NewSession()
if err != nil {
log.Fatalf("unable to create session: %s", err)
}
defer session.Close()
// Set up terminal modes
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
log.Fatalf("request for pseudo terminal failed: %s", err)
}
// Start remote shell
if err := session.Shell(); err != nil {
log.Fatalf("failed to start shell: %s", err)
}
}

412
modules/crypto/ssh/handshake.go Executable file
View File

@@ -0,0 +1,412 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto/rand"
"errors"
"fmt"
"io"
"log"
"net"
"sync"
)
// debugHandshake, if set, prints messages sent and received. Key
// exchange messages are printed as if DH were used, so the debug
// messages are wrong when using ECDH.
const debugHandshake = false
// keyingTransport is a packet based transport that supports key
// changes. It need not be thread-safe. It should pass through
// msgNewKeys in both directions.
type keyingTransport interface {
packetConn
// prepareKeyChange sets up a key change. The key change for a
// direction will be effected if a msgNewKeys message is sent
// or received.
prepareKeyChange(*algorithms, *kexResult) error
// getSessionID returns the session ID. prepareKeyChange must
// have been called once.
getSessionID() []byte
}
// rekeyingTransport is the interface of handshakeTransport that we
// (internally) expose to ClientConn and ServerConn.
type rekeyingTransport interface {
packetConn
// requestKeyChange asks the remote side to change keys. All
// writes are blocked until the key change succeeds, which is
// signaled by reading a msgNewKeys.
requestKeyChange() error
// getSessionID returns the session ID. This is only valid
// after the first key change has completed.
getSessionID() []byte
}
// handshakeTransport implements rekeying on top of a keyingTransport
// and offers a thread-safe writePacket() interface.
type handshakeTransport struct {
conn keyingTransport
config *Config
serverVersion []byte
clientVersion []byte
// hostKeys is non-empty if we are the server. In that case,
// it contains all host keys that can be used to sign the
// connection.
hostKeys []Signer
// hostKeyAlgorithms is non-empty if we are the client. In that case,
// we accept these key types from the server as host key.
hostKeyAlgorithms []string
// On read error, incoming is closed, and readError is set.
incoming chan []byte
readError error
// data for host key checking
hostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
dialAddress string
remoteAddr net.Addr
readSinceKex uint64
// Protects the writing side of the connection
mu sync.Mutex
cond *sync.Cond
sentInitPacket []byte
sentInitMsg *kexInitMsg
writtenSinceKex uint64
writeError error
}
func newHandshakeTransport(conn keyingTransport, config *Config, clientVersion, serverVersion []byte) *handshakeTransport {
t := &handshakeTransport{
conn: conn,
serverVersion: serverVersion,
clientVersion: clientVersion,
incoming: make(chan []byte, 16),
config: config,
}
t.cond = sync.NewCond(&t.mu)
return t
}
func newClientTransport(conn keyingTransport, clientVersion, serverVersion []byte, config *ClientConfig, dialAddr string, addr net.Addr) *handshakeTransport {
t := newHandshakeTransport(conn, &config.Config, clientVersion, serverVersion)
t.dialAddress = dialAddr
t.remoteAddr = addr
t.hostKeyCallback = config.HostKeyCallback
if config.HostKeyAlgorithms != nil {
t.hostKeyAlgorithms = config.HostKeyAlgorithms
} else {
t.hostKeyAlgorithms = supportedHostKeyAlgos
}
go t.readLoop()
return t
}
func newServerTransport(conn keyingTransport, clientVersion, serverVersion []byte, config *ServerConfig) *handshakeTransport {
t := newHandshakeTransport(conn, &config.Config, clientVersion, serverVersion)
t.hostKeys = config.hostKeys
go t.readLoop()
return t
}
func (t *handshakeTransport) getSessionID() []byte {
return t.conn.getSessionID()
}
func (t *handshakeTransport) id() string {
if len(t.hostKeys) > 0 {
return "server"
}
return "client"
}
func (t *handshakeTransport) readPacket() ([]byte, error) {
p, ok := <-t.incoming
if !ok {
return nil, t.readError
}
return p, nil
}
func (t *handshakeTransport) readLoop() {
for {
p, err := t.readOnePacket()
if err != nil {
t.readError = err
close(t.incoming)
break
}
if p[0] == msgIgnore || p[0] == msgDebug {
continue
}
t.incoming <- p
}
// If we can't read, declare the writing part dead too.
t.mu.Lock()
defer t.mu.Unlock()
if t.writeError == nil {
t.writeError = t.readError
}
t.cond.Broadcast()
}
func (t *handshakeTransport) readOnePacket() ([]byte, error) {
if t.readSinceKex > t.config.RekeyThreshold {
if err := t.requestKeyChange(); err != nil {
return nil, err
}
}
p, err := t.conn.readPacket()
if err != nil {
return nil, err
}
t.readSinceKex += uint64(len(p))
if debugHandshake {
msg, err := decode(p)
log.Printf("%s got %T %v (%v)", t.id(), msg, msg, err)
}
if p[0] != msgKexInit {
return p, nil
}
err = t.enterKeyExchange(p)
t.mu.Lock()
if err != nil {
// drop connection
t.conn.Close()
t.writeError = err
}
if debugHandshake {
log.Printf("%s exited key exchange, err %v", t.id(), err)
}
// Unblock writers.
t.sentInitMsg = nil
t.sentInitPacket = nil
t.cond.Broadcast()
t.writtenSinceKex = 0
t.mu.Unlock()
if err != nil {
return nil, err
}
t.readSinceKex = 0
return []byte{msgNewKeys}, nil
}
// sendKexInit sends a key change message, and returns the message
// that was sent. After initiating the key change, all writes will be
// blocked until the change is done, and a failed key change will
// close the underlying transport. This function is safe for
// concurrent use by multiple goroutines.
func (t *handshakeTransport) sendKexInit() (*kexInitMsg, []byte, error) {
t.mu.Lock()
defer t.mu.Unlock()
return t.sendKexInitLocked()
}
func (t *handshakeTransport) requestKeyChange() error {
_, _, err := t.sendKexInit()
return err
}
// sendKexInitLocked sends a key change message. t.mu must be locked
// while this happens.
func (t *handshakeTransport) sendKexInitLocked() (*kexInitMsg, []byte, error) {
// kexInits may be sent either in response to the other side,
// or because our side wants to initiate a key change, so we
// may have already sent a kexInit. In that case, don't send a
// second kexInit.
if t.sentInitMsg != nil {
return t.sentInitMsg, t.sentInitPacket, nil
}
msg := &kexInitMsg{
KexAlgos: t.config.KeyExchanges,
CiphersClientServer: t.config.Ciphers,
CiphersServerClient: t.config.Ciphers,
MACsClientServer: t.config.MACs,
MACsServerClient: t.config.MACs,
CompressionClientServer: supportedCompressions,
CompressionServerClient: supportedCompressions,
}
io.ReadFull(rand.Reader, msg.Cookie[:])
if len(t.hostKeys) > 0 {
for _, k := range t.hostKeys {
msg.ServerHostKeyAlgos = append(
msg.ServerHostKeyAlgos, k.PublicKey().Type())
}
} else {
msg.ServerHostKeyAlgos = t.hostKeyAlgorithms
}
packet := Marshal(msg)
// writePacket destroys the contents, so save a copy.
packetCopy := make([]byte, len(packet))
copy(packetCopy, packet)
if err := t.conn.writePacket(packetCopy); err != nil {
return nil, nil, err
}
t.sentInitMsg = msg
t.sentInitPacket = packet
return msg, packet, nil
}
func (t *handshakeTransport) writePacket(p []byte) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.writtenSinceKex > t.config.RekeyThreshold {
t.sendKexInitLocked()
}
for t.sentInitMsg != nil && t.writeError == nil {
t.cond.Wait()
}
if t.writeError != nil {
return t.writeError
}
t.writtenSinceKex += uint64(len(p))
switch p[0] {
case msgKexInit:
return errors.New("ssh: only handshakeTransport can send kexInit")
case msgNewKeys:
return errors.New("ssh: only handshakeTransport can send newKeys")
default:
return t.conn.writePacket(p)
}
}
func (t *handshakeTransport) Close() error {
return t.conn.Close()
}
// enterKeyExchange runs the key exchange.
func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
if debugHandshake {
log.Printf("%s entered key exchange", t.id())
}
myInit, myInitPacket, err := t.sendKexInit()
if err != nil {
return err
}
otherInit := &kexInitMsg{}
if err := Unmarshal(otherInitPacket, otherInit); err != nil {
return err
}
magics := handshakeMagics{
clientVersion: t.clientVersion,
serverVersion: t.serverVersion,
clientKexInit: otherInitPacket,
serverKexInit: myInitPacket,
}
clientInit := otherInit
serverInit := myInit
if len(t.hostKeys) == 0 {
clientInit = myInit
serverInit = otherInit
magics.clientKexInit = myInitPacket
magics.serverKexInit = otherInitPacket
}
algs, err := findAgreedAlgorithms(clientInit, serverInit)
if err != nil {
return err
}
// We don't send FirstKexFollows, but we handle receiving it.
if otherInit.FirstKexFollows && algs.kex != otherInit.KexAlgos[0] {
// other side sent a kex message for the wrong algorithm,
// which we have to ignore.
if _, err := t.conn.readPacket(); err != nil {
return err
}
}
kex, ok := kexAlgoMap[algs.kex]
if !ok {
return fmt.Errorf("ssh: unexpected key exchange algorithm %v", algs.kex)
}
var result *kexResult
if len(t.hostKeys) > 0 {
result, err = t.server(kex, algs, &magics)
} else {
result, err = t.client(kex, algs, &magics)
}
if err != nil {
return err
}
t.conn.prepareKeyChange(algs, result)
if err = t.conn.writePacket([]byte{msgNewKeys}); err != nil {
return err
}
if packet, err := t.conn.readPacket(); err != nil {
return err
} else if packet[0] != msgNewKeys {
return unexpectedMessageError(msgNewKeys, packet[0])
}
return nil
}
func (t *handshakeTransport) server(kex kexAlgorithm, algs *algorithms, magics *handshakeMagics) (*kexResult, error) {
var hostKey Signer
for _, k := range t.hostKeys {
if algs.hostKey == k.PublicKey().Type() {
hostKey = k
}
}
r, err := kex.Server(t.conn, t.config.Rand, magics, hostKey)
return r, err
}
func (t *handshakeTransport) client(kex kexAlgorithm, algs *algorithms, magics *handshakeMagics) (*kexResult, error) {
result, err := kex.Client(t.conn, t.config.Rand, magics)
if err != nil {
return nil, err
}
hostKey, err := ParsePublicKey(result.HostKey)
if err != nil {
return nil, err
}
if err := verifyHostKeySignature(hostKey, result); err != nil {
return nil, err
}
if t.hostKeyCallback != nil {
err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey)
if err != nil {
return nil, err
}
}
return result, nil
}

View File

@@ -0,0 +1,415 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"net"
"runtime"
"strings"
"sync"
"testing"
)
type testChecker struct {
calls []string
}
func (t *testChecker) Check(dialAddr string, addr net.Addr, key PublicKey) error {
if dialAddr == "bad" {
return fmt.Errorf("dialAddr is bad")
}
if tcpAddr, ok := addr.(*net.TCPAddr); !ok || tcpAddr == nil {
return fmt.Errorf("testChecker: got %T want *net.TCPAddr", addr)
}
t.calls = append(t.calls, fmt.Sprintf("%s %v %s %x", dialAddr, addr, key.Type(), key.Marshal()))
return nil
}
// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and
// therefore is buffered (net.Pipe deadlocks if both sides start with
// a write.)
func netPipe() (net.Conn, net.Conn, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, nil, err
}
defer listener.Close()
c1, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
return nil, nil, err
}
c2, err := listener.Accept()
if err != nil {
c1.Close()
return nil, nil, err
}
return c1, c2, nil
}
func handshakePair(clientConf *ClientConfig, addr string) (client *handshakeTransport, server *handshakeTransport, err error) {
a, b, err := netPipe()
if err != nil {
return nil, nil, err
}
trC := newTransport(a, rand.Reader, true)
trS := newTransport(b, rand.Reader, false)
clientConf.SetDefaults()
v := []byte("version")
client = newClientTransport(trC, v, v, clientConf, addr, a.RemoteAddr())
serverConf := &ServerConfig{}
serverConf.AddHostKey(testSigners["ecdsa"])
serverConf.AddHostKey(testSigners["rsa"])
serverConf.SetDefaults()
server = newServerTransport(trS, v, v, serverConf)
return client, server, nil
}
func TestHandshakeBasic(t *testing.T) {
if runtime.GOOS == "plan9" {
t.Skip("see golang.org/issue/7237")
}
checker := &testChecker{}
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
go func() {
// Client writes a bunch of stuff, and does a key
// change in the middle. This should not confuse the
// handshake in progress
for i := 0; i < 10; i++ {
p := []byte{msgRequestSuccess, byte(i)}
if err := trC.writePacket(p); err != nil {
t.Fatalf("sendPacket: %v", err)
}
if i == 5 {
// halfway through, we request a key change.
_, _, err := trC.sendKexInit()
if err != nil {
t.Fatalf("sendKexInit: %v", err)
}
}
}
trC.Close()
}()
// Server checks that client messages come in cleanly
i := 0
for {
p, err := trS.readPacket()
if err != nil {
break
}
if p[0] == msgNewKeys {
continue
}
want := []byte{msgRequestSuccess, byte(i)}
if bytes.Compare(p, want) != 0 {
t.Errorf("message %d: got %q, want %q", i, p, want)
}
i++
}
if i != 10 {
t.Errorf("received %d messages, want 10.", i)
}
// If all went well, we registered exactly 1 key change.
if len(checker.calls) != 1 {
t.Fatalf("got %d host key checks, want 1", len(checker.calls))
}
pub := testSigners["ecdsa"].PublicKey()
want := fmt.Sprintf("%s %v %s %x", "addr", trC.remoteAddr, pub.Type(), pub.Marshal())
if want != checker.calls[0] {
t.Errorf("got %q want %q for host key check", checker.calls[0], want)
}
}
func TestHandshakeError(t *testing.T) {
checker := &testChecker{}
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "bad")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
// send a packet
packet := []byte{msgRequestSuccess, 42}
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
// Now request a key change.
_, _, err = trC.sendKexInit()
if err != nil {
t.Errorf("sendKexInit: %v", err)
}
// the key change will fail, and afterwards we can't write.
if err := trC.writePacket([]byte{msgRequestSuccess, 43}); err == nil {
t.Errorf("writePacket after botched rekey succeeded.")
}
readback, err := trS.readPacket()
if err != nil {
t.Fatalf("server closed too soon: %v", err)
}
if bytes.Compare(readback, packet) != 0 {
t.Errorf("got %q want %q", readback, packet)
}
readback, err = trS.readPacket()
if err == nil {
t.Errorf("got a message %q after failed key change", readback)
}
}
func TestHandshakeTwice(t *testing.T) {
checker := &testChecker{}
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
// send a packet
packet := make([]byte, 5)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
// Now request a key change.
_, _, err = trC.sendKexInit()
if err != nil {
t.Errorf("sendKexInit: %v", err)
}
// Send another packet. Use a fresh one, since writePacket destroys.
packet = make([]byte, 5)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
// 2nd key change.
_, _, err = trC.sendKexInit()
if err != nil {
t.Errorf("sendKexInit: %v", err)
}
packet = make([]byte, 5)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
packet = make([]byte, 5)
packet[0] = msgRequestSuccess
for i := 0; i < 5; i++ {
msg, err := trS.readPacket()
if err != nil {
t.Fatalf("server closed too soon: %v", err)
}
if msg[0] == msgNewKeys {
continue
}
if bytes.Compare(msg, packet) != 0 {
t.Errorf("packet %d: got %q want %q", i, msg, packet)
}
}
if len(checker.calls) != 2 {
t.Errorf("got %d key changes, want 2", len(checker.calls))
}
}
func TestHandshakeAutoRekeyWrite(t *testing.T) {
checker := &testChecker{}
clientConf := &ClientConfig{HostKeyCallback: checker.Check}
clientConf.RekeyThreshold = 500
trC, trS, err := handshakePair(clientConf, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
for i := 0; i < 5; i++ {
packet := make([]byte, 251)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
}
j := 0
for ; j < 5; j++ {
_, err := trS.readPacket()
if err != nil {
break
}
}
if j != 5 {
t.Errorf("got %d, want 5 messages", j)
}
if len(checker.calls) != 2 {
t.Errorf("got %d key changes, wanted 2", len(checker.calls))
}
}
type syncChecker struct {
called chan int
}
func (t *syncChecker) Check(dialAddr string, addr net.Addr, key PublicKey) error {
t.called <- 1
return nil
}
func TestHandshakeAutoRekeyRead(t *testing.T) {
sync := &syncChecker{make(chan int, 2)}
clientConf := &ClientConfig{
HostKeyCallback: sync.Check,
}
clientConf.RekeyThreshold = 500
trC, trS, err := handshakePair(clientConf, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
packet := make([]byte, 501)
packet[0] = msgRequestSuccess
if err := trS.writePacket(packet); err != nil {
t.Fatalf("writePacket: %v", err)
}
// While we read out the packet, a key change will be
// initiated.
if _, err := trC.readPacket(); err != nil {
t.Fatalf("readPacket(client): %v", err)
}
<-sync.called
}
// errorKeyingTransport generates errors after a given number of
// read/write operations.
type errorKeyingTransport struct {
packetConn
readLeft, writeLeft int
}
func (n *errorKeyingTransport) prepareKeyChange(*algorithms, *kexResult) error {
return nil
}
func (n *errorKeyingTransport) getSessionID() []byte {
return nil
}
func (n *errorKeyingTransport) writePacket(packet []byte) error {
if n.writeLeft == 0 {
n.Close()
return errors.New("barf")
}
n.writeLeft--
return n.packetConn.writePacket(packet)
}
func (n *errorKeyingTransport) readPacket() ([]byte, error) {
if n.readLeft == 0 {
n.Close()
return nil, errors.New("barf")
}
n.readLeft--
return n.packetConn.readPacket()
}
func TestHandshakeErrorHandlingRead(t *testing.T) {
for i := 0; i < 20; i++ {
testHandshakeErrorHandlingN(t, i, -1)
}
}
func TestHandshakeErrorHandlingWrite(t *testing.T) {
for i := 0; i < 20; i++ {
testHandshakeErrorHandlingN(t, -1, i)
}
}
// testHandshakeErrorHandlingN runs handshakes, injecting errors. If
// handshakeTransport deadlocks, the go runtime will detect it and
// panic.
func testHandshakeErrorHandlingN(t *testing.T, readLimit, writeLimit int) {
msg := Marshal(&serviceRequestMsg{strings.Repeat("x", int(minRekeyThreshold)/4)})
a, b := memPipe()
defer a.Close()
defer b.Close()
key := testSigners["ecdsa"]
serverConf := Config{RekeyThreshold: minRekeyThreshold}
serverConf.SetDefaults()
serverConn := newHandshakeTransport(&errorKeyingTransport{a, readLimit, writeLimit}, &serverConf, []byte{'a'}, []byte{'b'})
serverConn.hostKeys = []Signer{key}
go serverConn.readLoop()
clientConf := Config{RekeyThreshold: 10 * minRekeyThreshold}
clientConf.SetDefaults()
clientConn := newHandshakeTransport(&errorKeyingTransport{b, -1, -1}, &clientConf, []byte{'a'}, []byte{'b'})
clientConn.hostKeyAlgorithms = []string{key.PublicKey().Type()}
go clientConn.readLoop()
var wg sync.WaitGroup
wg.Add(4)
for _, hs := range []packetConn{serverConn, clientConn} {
go func(c packetConn) {
for {
err := c.writePacket(msg)
if err != nil {
break
}
}
wg.Done()
}(hs)
go func(c packetConn) {
for {
_, err := c.readPacket()
if err != nil {
break
}
}
wg.Done()
}(hs)
}
wg.Wait()
}

526
modules/crypto/ssh/kex.go Executable file
View File

@@ -0,0 +1,526 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/subtle"
"crypto/rand"
"errors"
"io"
"math/big"
"golang.org/x/crypto/curve25519"
)
const (
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
kexAlgoECDH256 = "ecdh-sha2-nistp256"
kexAlgoECDH384 = "ecdh-sha2-nistp384"
kexAlgoECDH521 = "ecdh-sha2-nistp521"
kexAlgoCurve25519SHA256 = "curve25519-sha256@libssh.org"
)
// kexResult captures the outcome of a key exchange.
type kexResult struct {
// Session hash. See also RFC 4253, section 8.
H []byte
// Shared secret. See also RFC 4253, section 8.
K []byte
// Host key as hashed into H.
HostKey []byte
// Signature of H.
Signature []byte
// A cryptographic hash function that matches the security
// level of the key exchange algorithm. It is used for
// calculating H, and for deriving keys from H and K.
Hash crypto.Hash
// The session ID, which is the first H computed. This is used
// to signal data inside transport.
SessionID []byte
}
// handshakeMagics contains data that is always included in the
// session hash.
type handshakeMagics struct {
clientVersion, serverVersion []byte
clientKexInit, serverKexInit []byte
}
func (m *handshakeMagics) write(w io.Writer) {
writeString(w, m.clientVersion)
writeString(w, m.serverVersion)
writeString(w, m.clientKexInit)
writeString(w, m.serverKexInit)
}
// kexAlgorithm abstracts different key exchange algorithms.
type kexAlgorithm interface {
// Server runs server-side key agreement, signing the result
// with a hostkey.
Server(p packetConn, rand io.Reader, magics *handshakeMagics, s Signer) (*kexResult, error)
// Client runs the client-side key agreement. Caller is
// responsible for verifying the host key signature.
Client(p packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error)
}
// dhGroup is a multiplicative group suitable for implementing Diffie-Hellman key agreement.
type dhGroup struct {
g, p *big.Int
}
func (group *dhGroup) diffieHellman(theirPublic, myPrivate *big.Int) (*big.Int, error) {
if theirPublic.Sign() <= 0 || theirPublic.Cmp(group.p) >= 0 {
return nil, errors.New("ssh: DH parameter out of bounds")
}
return new(big.Int).Exp(theirPublic, myPrivate, group.p), nil
}
func (group *dhGroup) Client(c packetConn, randSource io.Reader, magics *handshakeMagics) (*kexResult, error) {
hashFunc := crypto.SHA1
x, err := rand.Int(randSource, group.p)
if err != nil {
return nil, err
}
X := new(big.Int).Exp(group.g, x, group.p)
kexDHInit := kexDHInitMsg{
X: X,
}
if err := c.writePacket(Marshal(&kexDHInit)); err != nil {
return nil, err
}
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var kexDHReply kexDHReplyMsg
if err = Unmarshal(packet, &kexDHReply); err != nil {
return nil, err
}
kInt, err := group.diffieHellman(kexDHReply.Y, x)
if err != nil {
return nil, err
}
h := hashFunc.New()
magics.write(h)
writeString(h, kexDHReply.HostKey)
writeInt(h, X)
writeInt(h, kexDHReply.Y)
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
return &kexResult{
H: h.Sum(nil),
K: K,
HostKey: kexDHReply.HostKey,
Signature: kexDHReply.Signature,
Hash: crypto.SHA1,
}, nil
}
func (group *dhGroup) Server(c packetConn, randSource io.Reader, magics *handshakeMagics, priv Signer) (result *kexResult, err error) {
hashFunc := crypto.SHA1
packet, err := c.readPacket()
if err != nil {
return
}
var kexDHInit kexDHInitMsg
if err = Unmarshal(packet, &kexDHInit); err != nil {
return
}
y, err := rand.Int(randSource, group.p)
if err != nil {
return
}
Y := new(big.Int).Exp(group.g, y, group.p)
kInt, err := group.diffieHellman(kexDHInit.X, y)
if err != nil {
return nil, err
}
hostKeyBytes := priv.PublicKey().Marshal()
h := hashFunc.New()
magics.write(h)
writeString(h, hostKeyBytes)
writeInt(h, kexDHInit.X)
writeInt(h, Y)
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
H := h.Sum(nil)
// H is already a hash, but the hostkey signing will apply its
// own key-specific hash algorithm.
sig, err := signAndMarshal(priv, randSource, H)
if err != nil {
return nil, err
}
kexDHReply := kexDHReplyMsg{
HostKey: hostKeyBytes,
Y: Y,
Signature: sig,
}
packet = Marshal(&kexDHReply)
err = c.writePacket(packet)
return &kexResult{
H: H,
K: K,
HostKey: hostKeyBytes,
Signature: sig,
Hash: crypto.SHA1,
}, nil
}
// ecdh performs Elliptic Curve Diffie-Hellman key exchange as
// described in RFC 5656, section 4.
type ecdh struct {
curve elliptic.Curve
}
func (kex *ecdh) Client(c packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error) {
ephKey, err := ecdsa.GenerateKey(kex.curve, rand)
if err != nil {
return nil, err
}
kexInit := kexECDHInitMsg{
ClientPubKey: elliptic.Marshal(kex.curve, ephKey.PublicKey.X, ephKey.PublicKey.Y),
}
serialized := Marshal(&kexInit)
if err := c.writePacket(serialized); err != nil {
return nil, err
}
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var reply kexECDHReplyMsg
if err = Unmarshal(packet, &reply); err != nil {
return nil, err
}
x, y, err := unmarshalECKey(kex.curve, reply.EphemeralPubKey)
if err != nil {
return nil, err
}
// generate shared secret
secret, _ := kex.curve.ScalarMult(x, y, ephKey.D.Bytes())
h := ecHash(kex.curve).New()
magics.write(h)
writeString(h, reply.HostKey)
writeString(h, kexInit.ClientPubKey)
writeString(h, reply.EphemeralPubKey)
K := make([]byte, intLength(secret))
marshalInt(K, secret)
h.Write(K)
return &kexResult{
H: h.Sum(nil),
K: K,
HostKey: reply.HostKey,
Signature: reply.Signature,
Hash: ecHash(kex.curve),
}, nil
}
// unmarshalECKey parses and checks an EC key.
func unmarshalECKey(curve elliptic.Curve, pubkey []byte) (x, y *big.Int, err error) {
x, y = elliptic.Unmarshal(curve, pubkey)
if x == nil {
return nil, nil, errors.New("ssh: elliptic.Unmarshal failure")
}
if !validateECPublicKey(curve, x, y) {
return nil, nil, errors.New("ssh: public key not on curve")
}
return x, y, nil
}
// validateECPublicKey checks that the point is a valid public key for
// the given curve. See [SEC1], 3.2.2
func validateECPublicKey(curve elliptic.Curve, x, y *big.Int) bool {
if x.Sign() == 0 && y.Sign() == 0 {
return false
}
if x.Cmp(curve.Params().P) >= 0 {
return false
}
if y.Cmp(curve.Params().P) >= 0 {
return false
}
if !curve.IsOnCurve(x, y) {
return false
}
// We don't check if N * PubKey == 0, since
//
// - the NIST curves have cofactor = 1, so this is implicit.
// (We don't foresee an implementation that supports non NIST
// curves)
//
// - for ephemeral keys, we don't need to worry about small
// subgroup attacks.
return true
}
func (kex *ecdh) Server(c packetConn, rand io.Reader, magics *handshakeMagics, priv Signer) (result *kexResult, err error) {
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var kexECDHInit kexECDHInitMsg
if err = Unmarshal(packet, &kexECDHInit); err != nil {
return nil, err
}
clientX, clientY, err := unmarshalECKey(kex.curve, kexECDHInit.ClientPubKey)
if err != nil {
return nil, err
}
// We could cache this key across multiple users/multiple
// connection attempts, but the benefit is small. OpenSSH
// generates a new key for each incoming connection.
ephKey, err := ecdsa.GenerateKey(kex.curve, rand)
if err != nil {
return nil, err
}
hostKeyBytes := priv.PublicKey().Marshal()
serializedEphKey := elliptic.Marshal(kex.curve, ephKey.PublicKey.X, ephKey.PublicKey.Y)
// generate shared secret
secret, _ := kex.curve.ScalarMult(clientX, clientY, ephKey.D.Bytes())
h := ecHash(kex.curve).New()
magics.write(h)
writeString(h, hostKeyBytes)
writeString(h, kexECDHInit.ClientPubKey)
writeString(h, serializedEphKey)
K := make([]byte, intLength(secret))
marshalInt(K, secret)
h.Write(K)
H := h.Sum(nil)
// H is already a hash, but the hostkey signing will apply its
// own key-specific hash algorithm.
sig, err := signAndMarshal(priv, rand, H)
if err != nil {
return nil, err
}
reply := kexECDHReplyMsg{
EphemeralPubKey: serializedEphKey,
HostKey: hostKeyBytes,
Signature: sig,
}
serialized := Marshal(&reply)
if err := c.writePacket(serialized); err != nil {
return nil, err
}
return &kexResult{
H: H,
K: K,
HostKey: reply.HostKey,
Signature: sig,
Hash: ecHash(kex.curve),
}, nil
}
var kexAlgoMap = map[string]kexAlgorithm{}
func init() {
// This is the group called diffie-hellman-group1-sha1 in RFC
// 4253 and Oakley Group 2 in RFC 2409.
p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", 16)
kexAlgoMap[kexAlgoDH1SHA1] = &dhGroup{
g: new(big.Int).SetInt64(2),
p: p,
}
// This is the group called diffie-hellman-group14-sha1 in RFC
// 4253 and Oakley Group 14 in RFC 3526.
p, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", 16)
kexAlgoMap[kexAlgoDH14SHA1] = &dhGroup{
g: new(big.Int).SetInt64(2),
p: p,
}
kexAlgoMap[kexAlgoECDH521] = &ecdh{elliptic.P521()}
kexAlgoMap[kexAlgoECDH384] = &ecdh{elliptic.P384()}
kexAlgoMap[kexAlgoECDH256] = &ecdh{elliptic.P256()}
kexAlgoMap[kexAlgoCurve25519SHA256] = &curve25519sha256{}
}
// curve25519sha256 implements the curve25519-sha256@libssh.org key
// agreement protocol, as described in
// https://git.libssh.org/projects/libssh.git/tree/doc/curve25519-sha256@libssh.org.txt
type curve25519sha256 struct{}
type curve25519KeyPair struct {
priv [32]byte
pub [32]byte
}
func (kp *curve25519KeyPair) generate(rand io.Reader) error {
if _, err := io.ReadFull(rand, kp.priv[:]); err != nil {
return err
}
curve25519.ScalarBaseMult(&kp.pub, &kp.priv)
return nil
}
// curve25519Zeros is just an array of 32 zero bytes so that we have something
// convenient to compare against in order to reject curve25519 points with the
// wrong order.
var curve25519Zeros [32]byte
func (kex *curve25519sha256) Client(c packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error) {
var kp curve25519KeyPair
if err := kp.generate(rand); err != nil {
return nil, err
}
if err := c.writePacket(Marshal(&kexECDHInitMsg{kp.pub[:]})); err != nil {
return nil, err
}
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var reply kexECDHReplyMsg
if err = Unmarshal(packet, &reply); err != nil {
return nil, err
}
if len(reply.EphemeralPubKey) != 32 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong length")
}
var servPub, secret [32]byte
copy(servPub[:], reply.EphemeralPubKey)
curve25519.ScalarMult(&secret, &kp.priv, &servPub)
if subtle.ConstantTimeCompare(secret[:], curve25519Zeros[:]) == 1 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong order")
}
h := crypto.SHA256.New()
magics.write(h)
writeString(h, reply.HostKey)
writeString(h, kp.pub[:])
writeString(h, reply.EphemeralPubKey)
kInt := new(big.Int).SetBytes(secret[:])
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
return &kexResult{
H: h.Sum(nil),
K: K,
HostKey: reply.HostKey,
Signature: reply.Signature,
Hash: crypto.SHA256,
}, nil
}
func (kex *curve25519sha256) Server(c packetConn, rand io.Reader, magics *handshakeMagics, priv Signer) (result *kexResult, err error) {
packet, err := c.readPacket()
if err != nil {
return
}
var kexInit kexECDHInitMsg
if err = Unmarshal(packet, &kexInit); err != nil {
return
}
if len(kexInit.ClientPubKey) != 32 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong length")
}
var kp curve25519KeyPair
if err := kp.generate(rand); err != nil {
return nil, err
}
var clientPub, secret [32]byte
copy(clientPub[:], kexInit.ClientPubKey)
curve25519.ScalarMult(&secret, &kp.priv, &clientPub)
if subtle.ConstantTimeCompare(secret[:], curve25519Zeros[:]) == 1 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong order")
}
hostKeyBytes := priv.PublicKey().Marshal()
h := crypto.SHA256.New()
magics.write(h)
writeString(h, hostKeyBytes)
writeString(h, kexInit.ClientPubKey)
writeString(h, kp.pub[:])
kInt := new(big.Int).SetBytes(secret[:])
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
H := h.Sum(nil)
sig, err := signAndMarshal(priv, rand, H)
if err != nil {
return nil, err
}
reply := kexECDHReplyMsg{
EphemeralPubKey: kp.pub[:],
HostKey: hostKeyBytes,
Signature: sig,
}
if err := c.writePacket(Marshal(&reply)); err != nil {
return nil, err
}
return &kexResult{
H: H,
K: K,
HostKey: hostKeyBytes,
Signature: sig,
Hash: crypto.SHA256,
}, nil
}

50
modules/crypto/ssh/kex_test.go Executable file
View File

@@ -0,0 +1,50 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
// Key exchange tests.
import (
"crypto/rand"
"reflect"
"testing"
)
func TestKexes(t *testing.T) {
type kexResultErr struct {
result *kexResult
err error
}
for name, kex := range kexAlgoMap {
a, b := memPipe()
s := make(chan kexResultErr, 1)
c := make(chan kexResultErr, 1)
var magics handshakeMagics
go func() {
r, e := kex.Client(a, rand.Reader, &magics)
a.Close()
c <- kexResultErr{r, e}
}()
go func() {
r, e := kex.Server(b, rand.Reader, &magics, testSigners["ecdsa"])
b.Close()
s <- kexResultErr{r, e}
}()
clientRes := <-c
serverRes := <-s
if clientRes.err != nil {
t.Errorf("client: %v", clientRes.err)
}
if serverRes.err != nil {
t.Errorf("server: %v", serverRes.err)
}
if !reflect.DeepEqual(clientRes.result, serverRes.result) {
t.Errorf("kex %q: mismatch %#v, %#v", name, clientRes.result, serverRes.result)
}
}
}

628
modules/crypto/ssh/keys.go Executable file
View File

@@ -0,0 +1,628 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
)
// These constants represent the algorithm names for key types supported by this
// package.
const (
KeyAlgoRSA = "ssh-rsa"
KeyAlgoDSA = "ssh-dss"
KeyAlgoECDSA256 = "ecdsa-sha2-nistp256"
KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
)
// parsePubKey parses a public key of the given algorithm.
// Use ParsePublicKey for keys with prepended algorithm.
func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err error) {
switch algo {
case KeyAlgoRSA:
return parseRSA(in)
case KeyAlgoDSA:
return parseDSA(in)
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
return parseECDSA(in)
case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01:
cert, err := parseCert(in, certToPrivAlgo(algo))
if err != nil {
return nil, nil, err
}
return cert, nil, nil
}
return nil, nil, fmt.Errorf("ssh: unknown key algorithm: %v", err)
}
// parseAuthorizedKey parses a public key in OpenSSH authorized_keys format
// (see sshd(8) manual page) once the options and key type fields have been
// removed.
func parseAuthorizedKey(in []byte) (out PublicKey, comment string, err error) {
in = bytes.TrimSpace(in)
i := bytes.IndexAny(in, " \t")
if i == -1 {
i = len(in)
}
base64Key := in[:i]
key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key)))
n, err := base64.StdEncoding.Decode(key, base64Key)
if err != nil {
return nil, "", err
}
key = key[:n]
out, err = ParsePublicKey(key)
if err != nil {
return nil, "", err
}
comment = string(bytes.TrimSpace(in[i:]))
return out, comment, nil
}
// ParseAuthorizedKeys parses a public key from an authorized_keys
// file used in OpenSSH according to the sshd(8) manual page.
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
for len(in) > 0 {
end := bytes.IndexByte(in, '\n')
if end != -1 {
rest = in[end+1:]
in = in[:end]
} else {
rest = nil
}
end = bytes.IndexByte(in, '\r')
if end != -1 {
in = in[:end]
}
in = bytes.TrimSpace(in)
if len(in) == 0 || in[0] == '#' {
in = rest
continue
}
i := bytes.IndexAny(in, " \t")
if i == -1 {
in = rest
continue
}
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
return out, comment, options, rest, nil
}
// No key type recognised. Maybe there's an options field at
// the beginning.
var b byte
inQuote := false
var candidateOptions []string
optionStart := 0
for i, b = range in {
isEnd := !inQuote && (b == ' ' || b == '\t')
if (b == ',' && !inQuote) || isEnd {
if i-optionStart > 0 {
candidateOptions = append(candidateOptions, string(in[optionStart:i]))
}
optionStart = i + 1
}
if isEnd {
break
}
if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
inQuote = !inQuote
}
}
for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
i++
}
if i == len(in) {
// Invalid line: unmatched quote
in = rest
continue
}
in = in[i:]
i = bytes.IndexAny(in, " \t")
if i == -1 {
in = rest
continue
}
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
options = candidateOptions
return out, comment, options, rest, nil
}
in = rest
continue
}
return nil, "", nil, nil, errors.New("ssh: no key found")
}
// ParsePublicKey parses an SSH public key formatted for use in
// the SSH wire protocol according to RFC 4253, section 6.6.
func ParsePublicKey(in []byte) (out PublicKey, err error) {
algo, in, ok := parseString(in)
if !ok {
return nil, errShortRead
}
var rest []byte
out, rest, err = parsePubKey(in, string(algo))
if len(rest) > 0 {
return nil, errors.New("ssh: trailing junk in public key")
}
return out, err
}
// MarshalAuthorizedKey serializes key for inclusion in an OpenSSH
// authorized_keys file. The return value ends with newline.
func MarshalAuthorizedKey(key PublicKey) []byte {
b := &bytes.Buffer{}
b.WriteString(key.Type())
b.WriteByte(' ')
e := base64.NewEncoder(base64.StdEncoding, b)
e.Write(key.Marshal())
e.Close()
b.WriteByte('\n')
return b.Bytes()
}
// PublicKey is an abstraction of different types of public keys.
type PublicKey interface {
// Type returns the key's type, e.g. "ssh-rsa".
Type() string
// Marshal returns the serialized key data in SSH wire format,
// with the name prefix.
Marshal() []byte
// Verify that sig is a signature on the given data using this
// key. This function will hash the data appropriately first.
Verify(data []byte, sig *Signature) error
}
// A Signer can create signatures that verify against a public key.
type Signer interface {
// PublicKey returns an associated PublicKey instance.
PublicKey() PublicKey
// Sign returns raw signature for the given data. This method
// will apply the hash specified for the keytype to the data.
Sign(rand io.Reader, data []byte) (*Signature, error)
}
type rsaPublicKey rsa.PublicKey
func (r *rsaPublicKey) Type() string {
return "ssh-rsa"
}
// parseRSA parses an RSA key according to RFC 4253, section 6.6.
func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
E *big.Int
N *big.Int
Rest []byte `ssh:"rest"`
}
if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}
if w.E.BitLen() > 24 {
return nil, nil, errors.New("ssh: exponent too large")
}
e := w.E.Int64()
if e < 3 || e&1 == 0 {
return nil, nil, errors.New("ssh: incorrect exponent")
}
var key rsa.PublicKey
key.E = int(e)
key.N = w.N
return (*rsaPublicKey)(&key), w.Rest, nil
}
func (r *rsaPublicKey) Marshal() []byte {
e := new(big.Int).SetInt64(int64(r.E))
wirekey := struct {
Name string
E *big.Int
N *big.Int
}{
KeyAlgoRSA,
e,
r.N,
}
return Marshal(&wirekey)
}
func (r *rsaPublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != r.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, r.Type())
}
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
return rsa.VerifyPKCS1v15((*rsa.PublicKey)(r), crypto.SHA1, digest, sig.Blob)
}
type rsaPrivateKey struct {
*rsa.PrivateKey
}
func (r *rsaPrivateKey) PublicKey() PublicKey {
return (*rsaPublicKey)(&r.PrivateKey.PublicKey)
}
func (r *rsaPrivateKey) Sign(rand io.Reader, data []byte) (*Signature, error) {
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
blob, err := rsa.SignPKCS1v15(rand, r.PrivateKey, crypto.SHA1, digest)
if err != nil {
return nil, err
}
return &Signature{
Format: r.PublicKey().Type(),
Blob: blob,
}, nil
}
type dsaPublicKey dsa.PublicKey
func (r *dsaPublicKey) Type() string {
return "ssh-dss"
}
// parseDSA parses an DSA key according to RFC 4253, section 6.6.
func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
P, Q, G, Y *big.Int
Rest []byte `ssh:"rest"`
}
if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}
key := &dsaPublicKey{
Parameters: dsa.Parameters{
P: w.P,
Q: w.Q,
G: w.G,
},
Y: w.Y,
}
return key, w.Rest, nil
}
func (k *dsaPublicKey) Marshal() []byte {
w := struct {
Name string
P, Q, G, Y *big.Int
}{
k.Type(),
k.P,
k.Q,
k.G,
k.Y,
}
return Marshal(&w)
}
func (k *dsaPublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != k.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type())
}
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
// Per RFC 4253, section 6.6,
// The value for 'dss_signature_blob' is encoded as a string containing
// r, followed by s (which are 160-bit integers, without lengths or
// padding, unsigned, and in network byte order).
// For DSS purposes, sig.Blob should be exactly 40 bytes in length.
if len(sig.Blob) != 40 {
return errors.New("ssh: DSA signature parse error")
}
r := new(big.Int).SetBytes(sig.Blob[:20])
s := new(big.Int).SetBytes(sig.Blob[20:])
if dsa.Verify((*dsa.PublicKey)(k), digest, r, s) {
return nil
}
return errors.New("ssh: signature did not verify")
}
type dsaPrivateKey struct {
*dsa.PrivateKey
}
func (k *dsaPrivateKey) PublicKey() PublicKey {
return (*dsaPublicKey)(&k.PrivateKey.PublicKey)
}
func (k *dsaPrivateKey) Sign(rand io.Reader, data []byte) (*Signature, error) {
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
r, s, err := dsa.Sign(rand, k.PrivateKey, digest)
if err != nil {
return nil, err
}
sig := make([]byte, 40)
rb := r.Bytes()
sb := s.Bytes()
copy(sig[20-len(rb):20], rb)
copy(sig[40-len(sb):], sb)
return &Signature{
Format: k.PublicKey().Type(),
Blob: sig,
}, nil
}
type ecdsaPublicKey ecdsa.PublicKey
func (key *ecdsaPublicKey) Type() string {
return "ecdsa-sha2-" + key.nistID()
}
func (key *ecdsaPublicKey) nistID() string {
switch key.Params().BitSize {
case 256:
return "nistp256"
case 384:
return "nistp384"
case 521:
return "nistp521"
}
panic("ssh: unsupported ecdsa key size")
}
func supportedEllipticCurve(curve elliptic.Curve) bool {
return curve == elliptic.P256() || curve == elliptic.P384() || curve == elliptic.P521()
}
// ecHash returns the hash to match the given elliptic curve, see RFC
// 5656, section 6.2.1
func ecHash(curve elliptic.Curve) crypto.Hash {
bitSize := curve.Params().BitSize
switch {
case bitSize <= 256:
return crypto.SHA256
case bitSize <= 384:
return crypto.SHA384
}
return crypto.SHA512
}
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
Curve string
KeyBytes []byte
Rest []byte `ssh:"rest"`
}
if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}
key := new(ecdsa.PublicKey)
switch w.Curve {
case "nistp256":
key.Curve = elliptic.P256()
case "nistp384":
key.Curve = elliptic.P384()
case "nistp521":
key.Curve = elliptic.P521()
default:
return nil, nil, errors.New("ssh: unsupported curve")
}
key.X, key.Y = elliptic.Unmarshal(key.Curve, w.KeyBytes)
if key.X == nil || key.Y == nil {
return nil, nil, errors.New("ssh: invalid curve point")
}
return (*ecdsaPublicKey)(key), w.Rest, nil
}
func (key *ecdsaPublicKey) Marshal() []byte {
// See RFC 5656, section 3.1.
keyBytes := elliptic.Marshal(key.Curve, key.X, key.Y)
w := struct {
Name string
ID string
Key []byte
}{
key.Type(),
key.nistID(),
keyBytes,
}
return Marshal(&w)
}
func (key *ecdsaPublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != key.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, key.Type())
}
h := ecHash(key.Curve).New()
h.Write(data)
digest := h.Sum(nil)
// Per RFC 5656, section 3.1.2,
// The ecdsa_signature_blob value has the following specific encoding:
// mpint r
// mpint s
var ecSig struct {
R *big.Int
S *big.Int
}
if err := Unmarshal(sig.Blob, &ecSig); err != nil {
return err
}
if ecdsa.Verify((*ecdsa.PublicKey)(key), digest, ecSig.R, ecSig.S) {
return nil
}
return errors.New("ssh: signature did not verify")
}
type ecdsaPrivateKey struct {
*ecdsa.PrivateKey
}
func (k *ecdsaPrivateKey) PublicKey() PublicKey {
return (*ecdsaPublicKey)(&k.PrivateKey.PublicKey)
}
func (k *ecdsaPrivateKey) Sign(rand io.Reader, data []byte) (*Signature, error) {
h := ecHash(k.PrivateKey.PublicKey.Curve).New()
h.Write(data)
digest := h.Sum(nil)
r, s, err := ecdsa.Sign(rand, k.PrivateKey, digest)
if err != nil {
return nil, err
}
sig := make([]byte, intLength(r)+intLength(s))
rest := marshalInt(sig, r)
marshalInt(rest, s)
return &Signature{
Format: k.PublicKey().Type(),
Blob: sig,
}, nil
}
// NewSignerFromKey takes a pointer to rsa, dsa or ecdsa PrivateKey
// returns a corresponding Signer instance. EC keys should use P256,
// P384 or P521.
func NewSignerFromKey(k interface{}) (Signer, error) {
var sshKey Signer
switch t := k.(type) {
case *rsa.PrivateKey:
sshKey = &rsaPrivateKey{t}
case *dsa.PrivateKey:
sshKey = &dsaPrivateKey{t}
case *ecdsa.PrivateKey:
if !supportedEllipticCurve(t.Curve) {
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
}
sshKey = &ecdsaPrivateKey{t}
default:
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
}
return sshKey, nil
}
// NewPublicKey takes a pointer to rsa, dsa or ecdsa PublicKey
// and returns a corresponding ssh PublicKey instance. EC keys should use P256, P384 or P521.
func NewPublicKey(k interface{}) (PublicKey, error) {
var sshKey PublicKey
switch t := k.(type) {
case *rsa.PublicKey:
sshKey = (*rsaPublicKey)(t)
case *ecdsa.PublicKey:
if !supportedEllipticCurve(t.Curve) {
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
}
sshKey = (*ecdsaPublicKey)(t)
case *dsa.PublicKey:
sshKey = (*dsaPublicKey)(t)
default:
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
}
return sshKey, nil
}
// ParsePrivateKey returns a Signer from a PEM encoded private key. It supports
// the same keys as ParseRawPrivateKey.
func ParsePrivateKey(pemBytes []byte) (Signer, error) {
key, err := ParseRawPrivateKey(pemBytes)
if err != nil {
return nil, err
}
return NewSignerFromKey(key)
}
// ParseRawPrivateKey returns a private key from a PEM encoded private key. It
// supports RSA (PKCS#1), DSA (OpenSSL), and ECDSA private keys.
func ParseRawPrivateKey(pemBytes []byte) (interface{}, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("ssh: no key found")
}
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "DSA PRIVATE KEY":
return ParseDSAPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
}
}
// ParseDSAPrivateKey returns a DSA private key from its ASN.1 DER encoding, as
// specified by the OpenSSL DSA man page.
func ParseDSAPrivateKey(der []byte) (*dsa.PrivateKey, error) {
var k struct {
Version int
P *big.Int
Q *big.Int
G *big.Int
Priv *big.Int
Pub *big.Int
}
rest, err := asn1.Unmarshal(der, &k)
if err != nil {
return nil, errors.New("ssh: failed to parse DSA key: " + err.Error())
}
if len(rest) > 0 {
return nil, errors.New("ssh: garbage after DSA key")
}
return &dsa.PrivateKey{
PublicKey: dsa.PublicKey{
Parameters: dsa.Parameters{
P: k.P,
Q: k.Q,
G: k.G,
},
Y: k.Priv,
},
X: k.Pub,
}, nil
}

306
modules/crypto/ssh/keys_test.go Executable file
View File

@@ -0,0 +1,306 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"fmt"
"reflect"
"strings"
"testing"
"github.com/gogits/gogs/modules/crypto/ssh/testdata"
)
func rawKey(pub PublicKey) interface{} {
switch k := pub.(type) {
case *rsaPublicKey:
return (*rsa.PublicKey)(k)
case *dsaPublicKey:
return (*dsa.PublicKey)(k)
case *ecdsaPublicKey:
return (*ecdsa.PublicKey)(k)
case *Certificate:
return k
}
panic("unknown key type")
}
func TestKeyMarshalParse(t *testing.T) {
for _, priv := range testSigners {
pub := priv.PublicKey()
roundtrip, err := ParsePublicKey(pub.Marshal())
if err != nil {
t.Errorf("ParsePublicKey(%T): %v", pub, err)
}
k1 := rawKey(pub)
k2 := rawKey(roundtrip)
if !reflect.DeepEqual(k1, k2) {
t.Errorf("got %#v in roundtrip, want %#v", k2, k1)
}
}
}
func TestUnsupportedCurves(t *testing.T) {
raw, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
if _, err = NewSignerFromKey(raw); err == nil || !strings.Contains(err.Error(), "only P256") {
t.Fatalf("NewPrivateKey should not succeed with P224, got: %v", err)
}
if _, err = NewPublicKey(&raw.PublicKey); err == nil || !strings.Contains(err.Error(), "only P256") {
t.Fatalf("NewPublicKey should not succeed with P224, got: %v", err)
}
}
func TestNewPublicKey(t *testing.T) {
for _, k := range testSigners {
raw := rawKey(k.PublicKey())
// Skip certificates, as NewPublicKey does not support them.
if _, ok := raw.(*Certificate); ok {
continue
}
pub, err := NewPublicKey(raw)
if err != nil {
t.Errorf("NewPublicKey(%#v): %v", raw, err)
}
if !reflect.DeepEqual(k.PublicKey(), pub) {
t.Errorf("NewPublicKey(%#v) = %#v, want %#v", raw, pub, k.PublicKey())
}
}
}
func TestKeySignVerify(t *testing.T) {
for _, priv := range testSigners {
pub := priv.PublicKey()
data := []byte("sign me")
sig, err := priv.Sign(rand.Reader, data)
if err != nil {
t.Fatalf("Sign(%T): %v", priv, err)
}
if err := pub.Verify(data, sig); err != nil {
t.Errorf("publicKey.Verify(%T): %v", priv, err)
}
sig.Blob[5]++
if err := pub.Verify(data, sig); err == nil {
t.Errorf("publicKey.Verify on broken sig did not fail")
}
}
}
func TestParseRSAPrivateKey(t *testing.T) {
key := testPrivateKeys["rsa"]
rsa, ok := key.(*rsa.PrivateKey)
if !ok {
t.Fatalf("got %T, want *rsa.PrivateKey", rsa)
}
if err := rsa.Validate(); err != nil {
t.Errorf("Validate: %v", err)
}
}
func TestParseECPrivateKey(t *testing.T) {
key := testPrivateKeys["ecdsa"]
ecKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
t.Fatalf("got %T, want *ecdsa.PrivateKey", ecKey)
}
if !validateECPublicKey(ecKey.Curve, ecKey.X, ecKey.Y) {
t.Fatalf("public key does not validate.")
}
}
func TestParseDSA(t *testing.T) {
// We actually exercise the ParsePrivateKey codepath here, as opposed to
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
// uses.
s, err := ParsePrivateKey(testdata.PEMBytes["dsa"])
if err != nil {
t.Fatalf("ParsePrivateKey returned error: %s", err)
}
data := []byte("sign me")
sig, err := s.Sign(rand.Reader, data)
if err != nil {
t.Fatalf("dsa.Sign: %v", err)
}
if err := s.PublicKey().Verify(data, sig); err != nil {
t.Errorf("Verify failed: %v", err)
}
}
// Tests for authorized_keys parsing.
// getTestKey returns a public key, and its base64 encoding.
func getTestKey() (PublicKey, string) {
k := testPublicKeys["rsa"]
b := &bytes.Buffer{}
e := base64.NewEncoder(base64.StdEncoding, b)
e.Write(k.Marshal())
e.Close()
return k, b.String()
}
func TestMarshalParsePublicKey(t *testing.T) {
pub, pubSerialized := getTestKey()
line := fmt.Sprintf("%s %s user@host", pub.Type(), pubSerialized)
authKeys := MarshalAuthorizedKey(pub)
actualFields := strings.Fields(string(authKeys))
if len(actualFields) == 0 {
t.Fatalf("failed authKeys: %v", authKeys)
}
// drop the comment
expectedFields := strings.Fields(line)[0:2]
if !reflect.DeepEqual(actualFields, expectedFields) {
t.Errorf("got %v, expected %v", actualFields, expectedFields)
}
actPub, _, _, _, err := ParseAuthorizedKey([]byte(line))
if err != nil {
t.Fatalf("cannot parse %v: %v", line, err)
}
if !reflect.DeepEqual(actPub, pub) {
t.Errorf("got %v, expected %v", actPub, pub)
}
}
type authResult struct {
pubKey PublicKey
options []string
comments string
rest string
ok bool
}
func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []authResult) {
rest := authKeys
var values []authResult
for len(rest) > 0 {
var r authResult
var err error
r.pubKey, r.comments, r.options, rest, err = ParseAuthorizedKey(rest)
r.ok = (err == nil)
t.Log(err)
r.rest = string(rest)
values = append(values, r)
}
if !reflect.DeepEqual(values, expected) {
t.Errorf("got %#v, expected %#v", values, expected)
}
}
func TestAuthorizedKeyBasic(t *testing.T) {
pub, pubSerialized := getTestKey()
line := "ssh-rsa " + pubSerialized + " user@host"
testAuthorizedKeys(t, []byte(line),
[]authResult{
{pub, nil, "user@host", "", true},
})
}
func TestAuth(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithOptions := []string{
`# comments to ignore before any keys...`,
``,
`env="HOME=/home/root",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`,
`# comments to ignore, along with a blank line`,
``,
`env="HOME=/home/root2" ssh-rsa ` + pubSerialized + ` user2@host2`,
``,
`# more comments, plus a invalid entry`,
`ssh-rsa data-that-will-not-parse user@host3`,
}
for _, eol := range []string{"\n", "\r\n"} {
authOptions := strings.Join(authWithOptions, eol)
rest2 := strings.Join(authWithOptions[3:], eol)
rest3 := strings.Join(authWithOptions[6:], eol)
testAuthorizedKeys(t, []byte(authOptions), []authResult{
{pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true},
{pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true},
{nil, nil, "", "", false},
})
}
}
func TestAuthWithQuotedSpaceInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedSpaceInEnv := []byte(`env="HOME=/home/root dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []authResult{
{pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true},
})
}
func TestAuthWithQuotedCommaInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedCommaInEnv := []byte(`env="HOME=/home/root,dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []authResult{
{pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true},
})
}
func TestAuthWithQuotedQuoteInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedQuoteInEnv := []byte(`env="HOME=/home/\"root dir",no-port-forwarding` + "\t" + `ssh-rsa` + "\t" + pubSerialized + ` user@host`)
authWithDoubleQuotedQuote := []byte(`no-port-forwarding,env="HOME=/home/ \"root dir\"" ssh-rsa ` + pubSerialized + "\t" + `user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []authResult{
{pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true},
})
testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []authResult{
{pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true},
})
}
func TestAuthWithInvalidSpace(t *testing.T) {
_, pubSerialized := getTestKey()
authWithInvalidSpace := []byte(`env="HOME=/home/root dir", no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
#more to follow but still no valid keys`)
testAuthorizedKeys(t, []byte(authWithInvalidSpace), []authResult{
{nil, nil, "", "", false},
})
}
func TestAuthWithMissingQuote(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithMissingQuote := []byte(`env="HOME=/home/root,no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
env="HOME=/home/root",shared-control ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithMissingQuote), []authResult{
{pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true},
})
}
func TestInvalidEntry(t *testing.T) {
authInvalid := []byte(`ssh-rsa`)
_, _, _, _, err := ParseAuthorizedKey(authInvalid)
if err == nil {
t.Errorf("got valid entry for %q", authInvalid)
}
}

57
modules/crypto/ssh/mac.go Executable file
View File

@@ -0,0 +1,57 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
// Message authentication support
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"hash"
)
type macMode struct {
keySize int
new func(key []byte) hash.Hash
}
// truncatingMAC wraps around a hash.Hash and truncates the output digest to
// a given size.
type truncatingMAC struct {
length int
hmac hash.Hash
}
func (t truncatingMAC) Write(data []byte) (int, error) {
return t.hmac.Write(data)
}
func (t truncatingMAC) Sum(in []byte) []byte {
out := t.hmac.Sum(in)
return out[:len(in)+t.length]
}
func (t truncatingMAC) Reset() {
t.hmac.Reset()
}
func (t truncatingMAC) Size() int {
return t.length
}
func (t truncatingMAC) BlockSize() int { return t.hmac.BlockSize() }
var macModes = map[string]*macMode{
"hmac-sha2-256": {32, func(key []byte) hash.Hash {
return hmac.New(sha256.New, key)
}},
"hmac-sha1": {20, func(key []byte) hash.Hash {
return hmac.New(sha1.New, key)
}},
"hmac-sha1-96": {20, func(key []byte) hash.Hash {
return truncatingMAC{12, hmac.New(sha1.New, key)}
}},
}

View File

@@ -0,0 +1,110 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"sync"
"testing"
)
// An in-memory packetConn. It is safe to call Close and writePacket
// from different goroutines.
type memTransport struct {
eof bool
pending [][]byte
write *memTransport
sync.Mutex
*sync.Cond
}
func (t *memTransport) readPacket() ([]byte, error) {
t.Lock()
defer t.Unlock()
for {
if len(t.pending) > 0 {
r := t.pending[0]
t.pending = t.pending[1:]
return r, nil
}
if t.eof {
return nil, io.EOF
}
t.Cond.Wait()
}
}
func (t *memTransport) closeSelf() error {
t.Lock()
defer t.Unlock()
if t.eof {
return io.EOF
}
t.eof = true
t.Cond.Broadcast()
return nil
}
func (t *memTransport) Close() error {
err := t.write.closeSelf()
t.closeSelf()
return err
}
func (t *memTransport) writePacket(p []byte) error {
t.write.Lock()
defer t.write.Unlock()
if t.write.eof {
return io.EOF
}
c := make([]byte, len(p))
copy(c, p)
t.write.pending = append(t.write.pending, c)
t.write.Cond.Signal()
return nil
}
func memPipe() (a, b packetConn) {
t1 := memTransport{}
t2 := memTransport{}
t1.write = &t2
t2.write = &t1
t1.Cond = sync.NewCond(&t1.Mutex)
t2.Cond = sync.NewCond(&t2.Mutex)
return &t1, &t2
}
func TestMemPipe(t *testing.T) {
a, b := memPipe()
if err := a.writePacket([]byte{42}); err != nil {
t.Fatalf("writePacket: %v", err)
}
if err := a.Close(); err != nil {
t.Fatal("Close: ", err)
}
p, err := b.readPacket()
if err != nil {
t.Fatal("readPacket: ", err)
}
if len(p) != 1 || p[0] != 42 {
t.Fatalf("got %v, want {42}", p)
}
p, err = b.readPacket()
if err != io.EOF {
t.Fatalf("got %v, %v, want EOF", p, err)
}
}
func TestDoubleClose(t *testing.T) {
a, _ := memPipe()
err := a.Close()
if err != nil {
t.Errorf("Close: %v", err)
}
err = a.Close()
if err != io.EOF {
t.Errorf("expect EOF on double close.")
}
}

725
modules/crypto/ssh/messages.go Executable file
View File

@@ -0,0 +1,725 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"reflect"
"strconv"
)
// These are SSH message type numbers. They are scattered around several
// documents but many were taken from [SSH-PARAMETERS].
const (
msgIgnore = 2
msgUnimplemented = 3
msgDebug = 4
msgNewKeys = 21
// Standard authentication messages
msgUserAuthSuccess = 52
msgUserAuthBanner = 53
)
// SSH messages:
//
// These structures mirror the wire format of the corresponding SSH messages.
// They are marshaled using reflection with the marshal and unmarshal functions
// in this file. The only wrinkle is that a final member of type []byte with a
// ssh tag of "rest" receives the remainder of a packet when unmarshaling.
// See RFC 4253, section 11.1.
const msgDisconnect = 1
// disconnectMsg is the message that signals a disconnect. It is also
// the error type returned from mux.Wait()
type disconnectMsg struct {
Reason uint32 `sshtype:"1"`
Message string
Language string
}
func (d *disconnectMsg) Error() string {
return fmt.Sprintf("ssh: disconnect reason %d: %s", d.Reason, d.Message)
}
// See RFC 4253, section 7.1.
const msgKexInit = 20
type kexInitMsg struct {
Cookie [16]byte `sshtype:"20"`
KexAlgos []string
ServerHostKeyAlgos []string
CiphersClientServer []string
CiphersServerClient []string
MACsClientServer []string
MACsServerClient []string
CompressionClientServer []string
CompressionServerClient []string
LanguagesClientServer []string
LanguagesServerClient []string
FirstKexFollows bool
Reserved uint32
}
// See RFC 4253, section 8.
// Diffie-Helman
const msgKexDHInit = 30
type kexDHInitMsg struct {
X *big.Int `sshtype:"30"`
}
const msgKexECDHInit = 30
type kexECDHInitMsg struct {
ClientPubKey []byte `sshtype:"30"`
}
const msgKexECDHReply = 31
type kexECDHReplyMsg struct {
HostKey []byte `sshtype:"31"`
EphemeralPubKey []byte
Signature []byte
}
const msgKexDHReply = 31
type kexDHReplyMsg struct {
HostKey []byte `sshtype:"31"`
Y *big.Int
Signature []byte
}
// See RFC 4253, section 10.
const msgServiceRequest = 5
type serviceRequestMsg struct {
Service string `sshtype:"5"`
}
// See RFC 4253, section 10.
const msgServiceAccept = 6
type serviceAcceptMsg struct {
Service string `sshtype:"6"`
}
// See RFC 4252, section 5.
const msgUserAuthRequest = 50
type userAuthRequestMsg struct {
User string `sshtype:"50"`
Service string
Method string
Payload []byte `ssh:"rest"`
}
// See RFC 4252, section 5.1
const msgUserAuthFailure = 51
type userAuthFailureMsg struct {
Methods []string `sshtype:"51"`
PartialSuccess bool
}
// See RFC 4256, section 3.2
const msgUserAuthInfoRequest = 60
const msgUserAuthInfoResponse = 61
type userAuthInfoRequestMsg struct {
User string `sshtype:"60"`
Instruction string
DeprecatedLanguage string
NumPrompts uint32
Prompts []byte `ssh:"rest"`
}
// See RFC 4254, section 5.1.
const msgChannelOpen = 90
type channelOpenMsg struct {
ChanType string `sshtype:"90"`
PeersId uint32
PeersWindow uint32
MaxPacketSize uint32
TypeSpecificData []byte `ssh:"rest"`
}
const msgChannelExtendedData = 95
const msgChannelData = 94
// See RFC 4254, section 5.1.
const msgChannelOpenConfirm = 91
type channelOpenConfirmMsg struct {
PeersId uint32 `sshtype:"91"`
MyId uint32
MyWindow uint32
MaxPacketSize uint32
TypeSpecificData []byte `ssh:"rest"`
}
// See RFC 4254, section 5.1.
const msgChannelOpenFailure = 92
type channelOpenFailureMsg struct {
PeersId uint32 `sshtype:"92"`
Reason RejectionReason
Message string
Language string
}
const msgChannelRequest = 98
type channelRequestMsg struct {
PeersId uint32 `sshtype:"98"`
Request string
WantReply bool
RequestSpecificData []byte `ssh:"rest"`
}
// See RFC 4254, section 5.4.
const msgChannelSuccess = 99
type channelRequestSuccessMsg struct {
PeersId uint32 `sshtype:"99"`
}
// See RFC 4254, section 5.4.
const msgChannelFailure = 100
type channelRequestFailureMsg struct {
PeersId uint32 `sshtype:"100"`
}
// See RFC 4254, section 5.3
const msgChannelClose = 97
type channelCloseMsg struct {
PeersId uint32 `sshtype:"97"`
}
// See RFC 4254, section 5.3
const msgChannelEOF = 96
type channelEOFMsg struct {
PeersId uint32 `sshtype:"96"`
}
// See RFC 4254, section 4
const msgGlobalRequest = 80
type globalRequestMsg struct {
Type string `sshtype:"80"`
WantReply bool
Data []byte `ssh:"rest"`
}
// See RFC 4254, section 4
const msgRequestSuccess = 81
type globalRequestSuccessMsg struct {
Data []byte `ssh:"rest" sshtype:"81"`
}
// See RFC 4254, section 4
const msgRequestFailure = 82
type globalRequestFailureMsg struct {
Data []byte `ssh:"rest" sshtype:"82"`
}
// See RFC 4254, section 5.2
const msgChannelWindowAdjust = 93
type windowAdjustMsg struct {
PeersId uint32 `sshtype:"93"`
AdditionalBytes uint32
}
// See RFC 4252, section 7
const msgUserAuthPubKeyOk = 60
type userAuthPubKeyOkMsg struct {
Algo string `sshtype:"60"`
PubKey []byte
}
// typeTag returns the type byte for the given type. The type should
// be struct.
func typeTag(structType reflect.Type) byte {
var tag byte
var tagStr string
tagStr = structType.Field(0).Tag.Get("sshtype")
i, err := strconv.Atoi(tagStr)
if err == nil {
tag = byte(i)
}
return tag
}
func fieldError(t reflect.Type, field int, problem string) error {
if problem != "" {
problem = ": " + problem
}
return fmt.Errorf("ssh: unmarshal error for field %s of type %s%s", t.Field(field).Name, t.Name(), problem)
}
var errShortRead = errors.New("ssh: short read")
// Unmarshal parses data in SSH wire format into a structure. The out
// argument should be a pointer to struct. If the first member of the
// struct has the "sshtype" tag set to a number in decimal, the packet
// must start that number. In case of error, Unmarshal returns a
// ParseError or UnexpectedMessageError.
func Unmarshal(data []byte, out interface{}) error {
v := reflect.ValueOf(out).Elem()
structType := v.Type()
expectedType := typeTag(structType)
if len(data) == 0 {
return parseError(expectedType)
}
if expectedType > 0 {
if data[0] != expectedType {
return unexpectedMessageError(expectedType, data[0])
}
data = data[1:]
}
var ok bool
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
t := field.Type()
switch t.Kind() {
case reflect.Bool:
if len(data) < 1 {
return errShortRead
}
field.SetBool(data[0] != 0)
data = data[1:]
case reflect.Array:
if t.Elem().Kind() != reflect.Uint8 {
return fieldError(structType, i, "array of unsupported type")
}
if len(data) < t.Len() {
return errShortRead
}
for j, n := 0, t.Len(); j < n; j++ {
field.Index(j).Set(reflect.ValueOf(data[j]))
}
data = data[t.Len():]
case reflect.Uint64:
var u64 uint64
if u64, data, ok = parseUint64(data); !ok {
return errShortRead
}
field.SetUint(u64)
case reflect.Uint32:
var u32 uint32
if u32, data, ok = parseUint32(data); !ok {
return errShortRead
}
field.SetUint(uint64(u32))
case reflect.Uint8:
if len(data) < 1 {
return errShortRead
}
field.SetUint(uint64(data[0]))
data = data[1:]
case reflect.String:
var s []byte
if s, data, ok = parseString(data); !ok {
return fieldError(structType, i, "")
}
field.SetString(string(s))
case reflect.Slice:
switch t.Elem().Kind() {
case reflect.Uint8:
if structType.Field(i).Tag.Get("ssh") == "rest" {
field.Set(reflect.ValueOf(data))
data = nil
} else {
var s []byte
if s, data, ok = parseString(data); !ok {
return errShortRead
}
field.Set(reflect.ValueOf(s))
}
case reflect.String:
var nl []string
if nl, data, ok = parseNameList(data); !ok {
return errShortRead
}
field.Set(reflect.ValueOf(nl))
default:
return fieldError(structType, i, "slice of unsupported type")
}
case reflect.Ptr:
if t == bigIntType {
var n *big.Int
if n, data, ok = parseInt(data); !ok {
return errShortRead
}
field.Set(reflect.ValueOf(n))
} else {
return fieldError(structType, i, "pointer to unsupported type")
}
default:
return fieldError(structType, i, "unsupported type")
}
}
if len(data) != 0 {
return parseError(expectedType)
}
return nil
}
// Marshal serializes the message in msg to SSH wire format. The msg
// argument should be a struct or pointer to struct. If the first
// member has the "sshtype" tag set to a number in decimal, that
// number is prepended to the result. If the last of member has the
// "ssh" tag set to "rest", its contents are appended to the output.
func Marshal(msg interface{}) []byte {
out := make([]byte, 0, 64)
return marshalStruct(out, msg)
}
func marshalStruct(out []byte, msg interface{}) []byte {
v := reflect.Indirect(reflect.ValueOf(msg))
msgType := typeTag(v.Type())
if msgType > 0 {
out = append(out, msgType)
}
for i, n := 0, v.NumField(); i < n; i++ {
field := v.Field(i)
switch t := field.Type(); t.Kind() {
case reflect.Bool:
var v uint8
if field.Bool() {
v = 1
}
out = append(out, v)
case reflect.Array:
if t.Elem().Kind() != reflect.Uint8 {
panic(fmt.Sprintf("array of non-uint8 in field %d: %T", i, field.Interface()))
}
for j, l := 0, t.Len(); j < l; j++ {
out = append(out, uint8(field.Index(j).Uint()))
}
case reflect.Uint32:
out = appendU32(out, uint32(field.Uint()))
case reflect.Uint64:
out = appendU64(out, uint64(field.Uint()))
case reflect.Uint8:
out = append(out, uint8(field.Uint()))
case reflect.String:
s := field.String()
out = appendInt(out, len(s))
out = append(out, s...)
case reflect.Slice:
switch t.Elem().Kind() {
case reflect.Uint8:
if v.Type().Field(i).Tag.Get("ssh") != "rest" {
out = appendInt(out, field.Len())
}
out = append(out, field.Bytes()...)
case reflect.String:
offset := len(out)
out = appendU32(out, 0)
if n := field.Len(); n > 0 {
for j := 0; j < n; j++ {
f := field.Index(j)
if j != 0 {
out = append(out, ',')
}
out = append(out, f.String()...)
}
// overwrite length value
binary.BigEndian.PutUint32(out[offset:], uint32(len(out)-offset-4))
}
default:
panic(fmt.Sprintf("slice of unknown type in field %d: %T", i, field.Interface()))
}
case reflect.Ptr:
if t == bigIntType {
var n *big.Int
nValue := reflect.ValueOf(&n)
nValue.Elem().Set(field)
needed := intLength(n)
oldLength := len(out)
if cap(out)-len(out) < needed {
newOut := make([]byte, len(out), 2*(len(out)+needed))
copy(newOut, out)
out = newOut
}
out = out[:oldLength+needed]
marshalInt(out[oldLength:], n)
} else {
panic(fmt.Sprintf("pointer to unknown type in field %d: %T", i, field.Interface()))
}
}
}
return out
}
var bigOne = big.NewInt(1)
func parseString(in []byte) (out, rest []byte, ok bool) {
if len(in) < 4 {
return
}
length := binary.BigEndian.Uint32(in)
in = in[4:]
if uint32(len(in)) < length {
return
}
out = in[:length]
rest = in[length:]
ok = true
return
}
var (
comma = []byte{','}
emptyNameList = []string{}
)
func parseNameList(in []byte) (out []string, rest []byte, ok bool) {
contents, rest, ok := parseString(in)
if !ok {
return
}
if len(contents) == 0 {
out = emptyNameList
return
}
parts := bytes.Split(contents, comma)
out = make([]string, len(parts))
for i, part := range parts {
out[i] = string(part)
}
return
}
func parseInt(in []byte) (out *big.Int, rest []byte, ok bool) {
contents, rest, ok := parseString(in)
if !ok {
return
}
out = new(big.Int)
if len(contents) > 0 && contents[0]&0x80 == 0x80 {
// This is a negative number
notBytes := make([]byte, len(contents))
for i := range notBytes {
notBytes[i] = ^contents[i]
}
out.SetBytes(notBytes)
out.Add(out, bigOne)
out.Neg(out)
} else {
// Positive number
out.SetBytes(contents)
}
ok = true
return
}
func parseUint32(in []byte) (uint32, []byte, bool) {
if len(in) < 4 {
return 0, nil, false
}
return binary.BigEndian.Uint32(in), in[4:], true
}
func parseUint64(in []byte) (uint64, []byte, bool) {
if len(in) < 8 {
return 0, nil, false
}
return binary.BigEndian.Uint64(in), in[8:], true
}
func intLength(n *big.Int) int {
length := 4 /* length bytes */
if n.Sign() < 0 {
nMinus1 := new(big.Int).Neg(n)
nMinus1.Sub(nMinus1, bigOne)
bitLen := nMinus1.BitLen()
if bitLen%8 == 0 {
// The number will need 0xff padding
length++
}
length += (bitLen + 7) / 8
} else if n.Sign() == 0 {
// A zero is the zero length string
} else {
bitLen := n.BitLen()
if bitLen%8 == 0 {
// The number will need 0x00 padding
length++
}
length += (bitLen + 7) / 8
}
return length
}
func marshalUint32(to []byte, n uint32) []byte {
binary.BigEndian.PutUint32(to, n)
return to[4:]
}
func marshalUint64(to []byte, n uint64) []byte {
binary.BigEndian.PutUint64(to, n)
return to[8:]
}
func marshalInt(to []byte, n *big.Int) []byte {
lengthBytes := to
to = to[4:]
length := 0
if n.Sign() < 0 {
// A negative number has to be converted to two's-complement
// form. So we'll subtract 1 and invert. If the
// most-significant-bit isn't set then we'll need to pad the
// beginning with 0xff in order to keep the number negative.
nMinus1 := new(big.Int).Neg(n)
nMinus1.Sub(nMinus1, bigOne)
bytes := nMinus1.Bytes()
for i := range bytes {
bytes[i] ^= 0xff
}
if len(bytes) == 0 || bytes[0]&0x80 == 0 {
to[0] = 0xff
to = to[1:]
length++
}
nBytes := copy(to, bytes)
to = to[nBytes:]
length += nBytes
} else if n.Sign() == 0 {
// A zero is the zero length string
} else {
bytes := n.Bytes()
if len(bytes) > 0 && bytes[0]&0x80 != 0 {
// We'll have to pad this with a 0x00 in order to
// stop it looking like a negative number.
to[0] = 0
to = to[1:]
length++
}
nBytes := copy(to, bytes)
to = to[nBytes:]
length += nBytes
}
lengthBytes[0] = byte(length >> 24)
lengthBytes[1] = byte(length >> 16)
lengthBytes[2] = byte(length >> 8)
lengthBytes[3] = byte(length)
return to
}
func writeInt(w io.Writer, n *big.Int) {
length := intLength(n)
buf := make([]byte, length)
marshalInt(buf, n)
w.Write(buf)
}
func writeString(w io.Writer, s []byte) {
var lengthBytes [4]byte
lengthBytes[0] = byte(len(s) >> 24)
lengthBytes[1] = byte(len(s) >> 16)
lengthBytes[2] = byte(len(s) >> 8)
lengthBytes[3] = byte(len(s))
w.Write(lengthBytes[:])
w.Write(s)
}
func stringLength(n int) int {
return 4 + n
}
func marshalString(to []byte, s []byte) []byte {
to[0] = byte(len(s) >> 24)
to[1] = byte(len(s) >> 16)
to[2] = byte(len(s) >> 8)
to[3] = byte(len(s))
to = to[4:]
copy(to, s)
return to[len(s):]
}
var bigIntType = reflect.TypeOf((*big.Int)(nil))
// Decode a packet into its corresponding message.
func decode(packet []byte) (interface{}, error) {
var msg interface{}
switch packet[0] {
case msgDisconnect:
msg = new(disconnectMsg)
case msgServiceRequest:
msg = new(serviceRequestMsg)
case msgServiceAccept:
msg = new(serviceAcceptMsg)
case msgKexInit:
msg = new(kexInitMsg)
case msgKexDHInit:
msg = new(kexDHInitMsg)
case msgKexDHReply:
msg = new(kexDHReplyMsg)
case msgUserAuthRequest:
msg = new(userAuthRequestMsg)
case msgUserAuthFailure:
msg = new(userAuthFailureMsg)
case msgUserAuthPubKeyOk:
msg = new(userAuthPubKeyOkMsg)
case msgGlobalRequest:
msg = new(globalRequestMsg)
case msgRequestSuccess:
msg = new(globalRequestSuccessMsg)
case msgRequestFailure:
msg = new(globalRequestFailureMsg)
case msgChannelOpen:
msg = new(channelOpenMsg)
case msgChannelOpenConfirm:
msg = new(channelOpenConfirmMsg)
case msgChannelOpenFailure:
msg = new(channelOpenFailureMsg)
case msgChannelWindowAdjust:
msg = new(windowAdjustMsg)
case msgChannelEOF:
msg = new(channelEOFMsg)
case msgChannelClose:
msg = new(channelCloseMsg)
case msgChannelRequest:
msg = new(channelRequestMsg)
case msgChannelSuccess:
msg = new(channelRequestSuccessMsg)
case msgChannelFailure:
msg = new(channelRequestFailureMsg)
default:
return nil, unexpectedMessageError(0, packet[0])
}
if err := Unmarshal(packet, msg); err != nil {
return nil, err
}
return msg, nil
}

View File

@@ -0,0 +1,254 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"math/big"
"math/rand"
"reflect"
"testing"
"testing/quick"
)
var intLengthTests = []struct {
val, length int
}{
{0, 4 + 0},
{1, 4 + 1},
{127, 4 + 1},
{128, 4 + 2},
{-1, 4 + 1},
}
func TestIntLength(t *testing.T) {
for _, test := range intLengthTests {
v := new(big.Int).SetInt64(int64(test.val))
length := intLength(v)
if length != test.length {
t.Errorf("For %d, got length %d but expected %d", test.val, length, test.length)
}
}
}
type msgAllTypes struct {
Bool bool `sshtype:"21"`
Array [16]byte
Uint64 uint64
Uint32 uint32
Uint8 uint8
String string
Strings []string
Bytes []byte
Int *big.Int
Rest []byte `ssh:"rest"`
}
func (t *msgAllTypes) Generate(rand *rand.Rand, size int) reflect.Value {
m := &msgAllTypes{}
m.Bool = rand.Intn(2) == 1
randomBytes(m.Array[:], rand)
m.Uint64 = uint64(rand.Int63n(1<<63 - 1))
m.Uint32 = uint32(rand.Intn((1 << 31) - 1))
m.Uint8 = uint8(rand.Intn(1 << 8))
m.String = string(m.Array[:])
m.Strings = randomNameList(rand)
m.Bytes = m.Array[:]
m.Int = randomInt(rand)
m.Rest = m.Array[:]
return reflect.ValueOf(m)
}
func TestMarshalUnmarshal(t *testing.T) {
rand := rand.New(rand.NewSource(0))
iface := &msgAllTypes{}
ty := reflect.ValueOf(iface).Type()
n := 100
if testing.Short() {
n = 5
}
for j := 0; j < n; j++ {
v, ok := quick.Value(ty, rand)
if !ok {
t.Errorf("failed to create value")
break
}
m1 := v.Elem().Interface()
m2 := iface
marshaled := Marshal(m1)
if err := Unmarshal(marshaled, m2); err != nil {
t.Errorf("Unmarshal %#v: %s", m1, err)
break
}
if !reflect.DeepEqual(v.Interface(), m2) {
t.Errorf("got: %#v\nwant:%#v\n%x", m2, m1, marshaled)
break
}
}
}
func TestUnmarshalEmptyPacket(t *testing.T) {
var b []byte
var m channelRequestSuccessMsg
if err := Unmarshal(b, &m); err == nil {
t.Fatalf("unmarshal of empty slice succeeded")
}
}
func TestUnmarshalUnexpectedPacket(t *testing.T) {
type S struct {
I uint32 `sshtype:"43"`
S string
B bool
}
s := S{11, "hello", true}
packet := Marshal(s)
packet[0] = 42
roundtrip := S{}
err := Unmarshal(packet, &roundtrip)
if err == nil {
t.Fatal("expected error, not nil")
}
}
func TestMarshalPtr(t *testing.T) {
s := struct {
S string
}{"hello"}
m1 := Marshal(s)
m2 := Marshal(&s)
if !bytes.Equal(m1, m2) {
t.Errorf("got %q, want %q for marshaled pointer", m2, m1)
}
}
func TestBareMarshalUnmarshal(t *testing.T) {
type S struct {
I uint32
S string
B bool
}
s := S{42, "hello", true}
packet := Marshal(s)
roundtrip := S{}
Unmarshal(packet, &roundtrip)
if !reflect.DeepEqual(s, roundtrip) {
t.Errorf("got %#v, want %#v", roundtrip, s)
}
}
func TestBareMarshal(t *testing.T) {
type S2 struct {
I uint32
}
s := S2{42}
packet := Marshal(s)
i, rest, ok := parseUint32(packet)
if len(rest) > 0 || !ok {
t.Errorf("parseInt(%q): parse error", packet)
}
if i != s.I {
t.Errorf("got %d, want %d", i, s.I)
}
}
func TestUnmarshalShortKexInitPacket(t *testing.T) {
// This used to panic.
// Issue 11348
packet := []byte{0x14, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0xff, 0xff, 0xff, 0xff}
kim := &kexInitMsg{}
if err := Unmarshal(packet, kim); err == nil {
t.Error("truncated packet unmarshaled without error")
}
}
func randomBytes(out []byte, rand *rand.Rand) {
for i := 0; i < len(out); i++ {
out[i] = byte(rand.Int31())
}
}
func randomNameList(rand *rand.Rand) []string {
ret := make([]string, rand.Int31()&15)
for i := range ret {
s := make([]byte, 1+(rand.Int31()&15))
for j := range s {
s[j] = 'a' + uint8(rand.Int31()&15)
}
ret[i] = string(s)
}
return ret
}
func randomInt(rand *rand.Rand) *big.Int {
return new(big.Int).SetInt64(int64(int32(rand.Uint32())))
}
func (*kexInitMsg) Generate(rand *rand.Rand, size int) reflect.Value {
ki := &kexInitMsg{}
randomBytes(ki.Cookie[:], rand)
ki.KexAlgos = randomNameList(rand)
ki.ServerHostKeyAlgos = randomNameList(rand)
ki.CiphersClientServer = randomNameList(rand)
ki.CiphersServerClient = randomNameList(rand)
ki.MACsClientServer = randomNameList(rand)
ki.MACsServerClient = randomNameList(rand)
ki.CompressionClientServer = randomNameList(rand)
ki.CompressionServerClient = randomNameList(rand)
ki.LanguagesClientServer = randomNameList(rand)
ki.LanguagesServerClient = randomNameList(rand)
if rand.Int31()&1 == 1 {
ki.FirstKexFollows = true
}
return reflect.ValueOf(ki)
}
func (*kexDHInitMsg) Generate(rand *rand.Rand, size int) reflect.Value {
dhi := &kexDHInitMsg{}
dhi.X = randomInt(rand)
return reflect.ValueOf(dhi)
}
var (
_kexInitMsg = new(kexInitMsg).Generate(rand.New(rand.NewSource(0)), 10).Elem().Interface()
_kexDHInitMsg = new(kexDHInitMsg).Generate(rand.New(rand.NewSource(0)), 10).Elem().Interface()
_kexInit = Marshal(_kexInitMsg)
_kexDHInit = Marshal(_kexDHInitMsg)
)
func BenchmarkMarshalKexInitMsg(b *testing.B) {
for i := 0; i < b.N; i++ {
Marshal(_kexInitMsg)
}
}
func BenchmarkUnmarshalKexInitMsg(b *testing.B) {
m := new(kexInitMsg)
for i := 0; i < b.N; i++ {
Unmarshal(_kexInit, m)
}
}
func BenchmarkMarshalKexDHInitMsg(b *testing.B) {
for i := 0; i < b.N; i++ {
Marshal(_kexDHInitMsg)
}
}
func BenchmarkUnmarshalKexDHInitMsg(b *testing.B) {
m := new(kexDHInitMsg)
for i := 0; i < b.N; i++ {
Unmarshal(_kexDHInit, m)
}
}

356
modules/crypto/ssh/mux.go Executable file
View File

@@ -0,0 +1,356 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"encoding/binary"
"fmt"
"io"
"log"
"sync"
"sync/atomic"
)
// debugMux, if set, causes messages in the connection protocol to be
// logged.
const debugMux = false
// chanList is a thread safe channel list.
type chanList struct {
// protects concurrent access to chans
sync.Mutex
// chans are indexed by the local id of the channel, which the
// other side should send in the PeersId field.
chans []*channel
// This is a debugging aid: it offsets all IDs by this
// amount. This helps distinguish otherwise identical
// server/client muxes
offset uint32
}
// Assigns a channel ID to the given channel.
func (c *chanList) add(ch *channel) uint32 {
c.Lock()
defer c.Unlock()
for i := range c.chans {
if c.chans[i] == nil {
c.chans[i] = ch
return uint32(i) + c.offset
}
}
c.chans = append(c.chans, ch)
return uint32(len(c.chans)-1) + c.offset
}
// getChan returns the channel for the given ID.
func (c *chanList) getChan(id uint32) *channel {
id -= c.offset
c.Lock()
defer c.Unlock()
if id < uint32(len(c.chans)) {
return c.chans[id]
}
return nil
}
func (c *chanList) remove(id uint32) {
id -= c.offset
c.Lock()
if id < uint32(len(c.chans)) {
c.chans[id] = nil
}
c.Unlock()
}
// dropAll forgets all channels it knows, returning them in a slice.
func (c *chanList) dropAll() []*channel {
c.Lock()
defer c.Unlock()
var r []*channel
for _, ch := range c.chans {
if ch == nil {
continue
}
r = append(r, ch)
}
c.chans = nil
return r
}
// mux represents the state for the SSH connection protocol, which
// multiplexes many channels onto a single packet transport.
type mux struct {
conn packetConn
chanList chanList
incomingChannels chan NewChannel
globalSentMu sync.Mutex
globalResponses chan interface{}
incomingRequests chan *Request
errCond *sync.Cond
err error
}
// When debugging, each new chanList instantiation has a different
// offset.
var globalOff uint32
func (m *mux) Wait() error {
m.errCond.L.Lock()
defer m.errCond.L.Unlock()
for m.err == nil {
m.errCond.Wait()
}
return m.err
}
// newMux returns a mux that runs over the given connection.
func newMux(p packetConn) *mux {
m := &mux{
conn: p,
incomingChannels: make(chan NewChannel, 16),
globalResponses: make(chan interface{}, 1),
incomingRequests: make(chan *Request, 16),
errCond: newCond(),
}
if debugMux {
m.chanList.offset = atomic.AddUint32(&globalOff, 1)
}
go m.loop()
return m
}
func (m *mux) sendMessage(msg interface{}) error {
p := Marshal(msg)
return m.conn.writePacket(p)
}
func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) {
if wantReply {
m.globalSentMu.Lock()
defer m.globalSentMu.Unlock()
}
if err := m.sendMessage(globalRequestMsg{
Type: name,
WantReply: wantReply,
Data: payload,
}); err != nil {
return false, nil, err
}
if !wantReply {
return false, nil, nil
}
msg, ok := <-m.globalResponses
if !ok {
return false, nil, io.EOF
}
switch msg := msg.(type) {
case *globalRequestFailureMsg:
return false, msg.Data, nil
case *globalRequestSuccessMsg:
return true, msg.Data, nil
default:
return false, nil, fmt.Errorf("ssh: unexpected response to request: %#v", msg)
}
}
// ackRequest must be called after processing a global request that
// has WantReply set.
func (m *mux) ackRequest(ok bool, data []byte) error {
if ok {
return m.sendMessage(globalRequestSuccessMsg{Data: data})
}
return m.sendMessage(globalRequestFailureMsg{Data: data})
}
// TODO(hanwen): Disconnect is a transport layer message. We should
// probably send and receive Disconnect somewhere in the transport
// code.
// Disconnect sends a disconnect message.
func (m *mux) Disconnect(reason uint32, message string) error {
return m.sendMessage(disconnectMsg{
Reason: reason,
Message: message,
})
}
func (m *mux) Close() error {
return m.conn.Close()
}
// loop runs the connection machine. It will process packets until an
// error is encountered. To synchronize on loop exit, use mux.Wait.
func (m *mux) loop() {
var err error
for err == nil {
err = m.onePacket()
}
for _, ch := range m.chanList.dropAll() {
ch.close()
}
close(m.incomingChannels)
close(m.incomingRequests)
close(m.globalResponses)
m.conn.Close()
m.errCond.L.Lock()
m.err = err
m.errCond.Broadcast()
m.errCond.L.Unlock()
if debugMux {
log.Println("loop exit", err)
}
}
// onePacket reads and processes one packet.
func (m *mux) onePacket() error {
packet, err := m.conn.readPacket()
if err != nil {
return err
}
if debugMux {
if packet[0] == msgChannelData || packet[0] == msgChannelExtendedData {
log.Printf("decoding(%d): data packet - %d bytes", m.chanList.offset, len(packet))
} else {
p, _ := decode(packet)
log.Printf("decoding(%d): %d %#v - %d bytes", m.chanList.offset, packet[0], p, len(packet))
}
}
switch packet[0] {
case msgNewKeys:
// Ignore notification of key change.
return nil
case msgDisconnect:
return m.handleDisconnect(packet)
case msgChannelOpen:
return m.handleChannelOpen(packet)
case msgGlobalRequest, msgRequestSuccess, msgRequestFailure:
return m.handleGlobalPacket(packet)
}
// assume a channel packet.
if len(packet) < 5 {
return parseError(packet[0])
}
id := binary.BigEndian.Uint32(packet[1:])
ch := m.chanList.getChan(id)
if ch == nil {
return fmt.Errorf("ssh: invalid channel %d", id)
}
return ch.handlePacket(packet)
}
func (m *mux) handleDisconnect(packet []byte) error {
var d disconnectMsg
if err := Unmarshal(packet, &d); err != nil {
return err
}
if debugMux {
log.Printf("caught disconnect: %v", d)
}
return &d
}
func (m *mux) handleGlobalPacket(packet []byte) error {
msg, err := decode(packet)
if err != nil {
return err
}
switch msg := msg.(type) {
case *globalRequestMsg:
m.incomingRequests <- &Request{
Type: msg.Type,
WantReply: msg.WantReply,
Payload: msg.Data,
mux: m,
}
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
m.globalResponses <- msg
default:
panic(fmt.Sprintf("not a global message %#v", msg))
}
return nil
}
// handleChannelOpen schedules a channel to be Accept()ed.
func (m *mux) handleChannelOpen(packet []byte) error {
var msg channelOpenMsg
if err := Unmarshal(packet, &msg); err != nil {
return err
}
if msg.MaxPacketSize < minPacketLength || msg.MaxPacketSize > 1<<31 {
failMsg := channelOpenFailureMsg{
PeersId: msg.PeersId,
Reason: ConnectionFailed,
Message: "invalid request",
Language: "en_US.UTF-8",
}
return m.sendMessage(failMsg)
}
c := m.newChannel(msg.ChanType, channelInbound, msg.TypeSpecificData)
c.remoteId = msg.PeersId
c.maxRemotePayload = msg.MaxPacketSize
c.remoteWin.add(msg.PeersWindow)
m.incomingChannels <- c
return nil
}
func (m *mux) OpenChannel(chanType string, extra []byte) (Channel, <-chan *Request, error) {
ch, err := m.openChannel(chanType, extra)
if err != nil {
return nil, nil, err
}
return ch, ch.incomingRequests, nil
}
func (m *mux) openChannel(chanType string, extra []byte) (*channel, error) {
ch := m.newChannel(chanType, channelOutbound, extra)
ch.maxIncomingPayload = channelMaxPacket
open := channelOpenMsg{
ChanType: chanType,
PeersWindow: ch.myWindow,
MaxPacketSize: ch.maxIncomingPayload,
TypeSpecificData: extra,
PeersId: ch.localId,
}
if err := m.sendMessage(open); err != nil {
return nil, err
}
switch msg := (<-ch.msg).(type) {
case *channelOpenConfirmMsg:
return ch, nil
case *channelOpenFailureMsg:
return nil, &OpenChannelError{msg.Reason, msg.Message}
default:
return nil, fmt.Errorf("ssh: unexpected packet in response to channel open: %T", msg)
}
}

525
modules/crypto/ssh/mux_test.go Executable file
View File

@@ -0,0 +1,525 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"io/ioutil"
"sync"
"testing"
)
func muxPair() (*mux, *mux) {
a, b := memPipe()
s := newMux(a)
c := newMux(b)
return s, c
}
// Returns both ends of a channel, and the mux for the the 2nd
// channel.
func channelPair(t *testing.T) (*channel, *channel, *mux) {
c, s := muxPair()
res := make(chan *channel, 1)
go func() {
newCh, ok := <-s.incomingChannels
if !ok {
t.Fatalf("No incoming channel")
}
if newCh.ChannelType() != "chan" {
t.Fatalf("got type %q want chan", newCh.ChannelType())
}
ch, _, err := newCh.Accept()
if err != nil {
t.Fatalf("Accept %v", err)
}
res <- ch.(*channel)
}()
ch, err := c.openChannel("chan", nil)
if err != nil {
t.Fatalf("OpenChannel: %v", err)
}
return <-res, ch, c
}
// Test that stderr and stdout can be addressed from different
// goroutines. This is intended for use with the race detector.
func TestMuxChannelExtendedThreadSafety(t *testing.T) {
writer, reader, mux := channelPair(t)
defer writer.Close()
defer reader.Close()
defer mux.Close()
var wr, rd sync.WaitGroup
magic := "hello world"
wr.Add(2)
go func() {
io.WriteString(writer, magic)
wr.Done()
}()
go func() {
io.WriteString(writer.Stderr(), magic)
wr.Done()
}()
rd.Add(2)
go func() {
c, err := ioutil.ReadAll(reader)
if string(c) != magic {
t.Fatalf("stdout read got %q, want %q (error %s)", c, magic, err)
}
rd.Done()
}()
go func() {
c, err := ioutil.ReadAll(reader.Stderr())
if string(c) != magic {
t.Fatalf("stderr read got %q, want %q (error %s)", c, magic, err)
}
rd.Done()
}()
wr.Wait()
writer.CloseWrite()
rd.Wait()
}
func TestMuxReadWrite(t *testing.T) {
s, c, mux := channelPair(t)
defer s.Close()
defer c.Close()
defer mux.Close()
magic := "hello world"
magicExt := "hello stderr"
go func() {
_, err := s.Write([]byte(magic))
if err != nil {
t.Fatalf("Write: %v", err)
}
_, err = s.Extended(1).Write([]byte(magicExt))
if err != nil {
t.Fatalf("Write: %v", err)
}
err = s.Close()
if err != nil {
t.Fatalf("Close: %v", err)
}
}()
var buf [1024]byte
n, err := c.Read(buf[:])
if err != nil {
t.Fatalf("server Read: %v", err)
}
got := string(buf[:n])
if got != magic {
t.Fatalf("server: got %q want %q", got, magic)
}
n, err = c.Extended(1).Read(buf[:])
if err != nil {
t.Fatalf("server Read: %v", err)
}
got = string(buf[:n])
if got != magicExt {
t.Fatalf("server: got %q want %q", got, magic)
}
}
func TestMuxChannelOverflow(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
wDone := make(chan int, 1)
go func() {
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
writer.Write(make([]byte, 1))
wDone <- 1
}()
writer.remoteWin.waitWriterBlocked()
// Send 1 byte.
packet := make([]byte, 1+4+4+1)
packet[0] = msgChannelData
marshalUint32(packet[1:], writer.remoteId)
marshalUint32(packet[5:], uint32(1))
packet[9] = 42
if err := writer.mux.conn.writePacket(packet); err != nil {
t.Errorf("could not send packet")
}
if _, err := reader.SendRequest("hello", true, nil); err == nil {
t.Errorf("SendRequest succeeded.")
}
<-wDone
}
func TestMuxChannelCloseWriteUnblock(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
wDone := make(chan int, 1)
go func() {
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
if _, err := writer.Write(make([]byte, 1)); err != io.EOF {
t.Errorf("got %v, want EOF for unblock write", err)
}
wDone <- 1
}()
writer.remoteWin.waitWriterBlocked()
reader.Close()
<-wDone
}
func TestMuxConnectionCloseWriteUnblock(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
wDone := make(chan int, 1)
go func() {
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
if _, err := writer.Write(make([]byte, 1)); err != io.EOF {
t.Errorf("got %v, want EOF for unblock write", err)
}
wDone <- 1
}()
writer.remoteWin.waitWriterBlocked()
mux.Close()
<-wDone
}
func TestMuxReject(t *testing.T) {
client, server := muxPair()
defer server.Close()
defer client.Close()
go func() {
ch, ok := <-server.incomingChannels
if !ok {
t.Fatalf("Accept")
}
if ch.ChannelType() != "ch" || string(ch.ExtraData()) != "extra" {
t.Fatalf("unexpected channel: %q, %q", ch.ChannelType(), ch.ExtraData())
}
ch.Reject(RejectionReason(42), "message")
}()
ch, err := client.openChannel("ch", []byte("extra"))
if ch != nil {
t.Fatal("openChannel not rejected")
}
ocf, ok := err.(*OpenChannelError)
if !ok {
t.Errorf("got %#v want *OpenChannelError", err)
} else if ocf.Reason != 42 || ocf.Message != "message" {
t.Errorf("got %#v, want {Reason: 42, Message: %q}", ocf, "message")
}
want := "ssh: rejected: unknown reason 42 (message)"
if err.Error() != want {
t.Errorf("got %q, want %q", err.Error(), want)
}
}
func TestMuxChannelRequest(t *testing.T) {
client, server, mux := channelPair(t)
defer server.Close()
defer client.Close()
defer mux.Close()
var received int
var wg sync.WaitGroup
wg.Add(1)
go func() {
for r := range server.incomingRequests {
received++
r.Reply(r.Type == "yes", nil)
}
wg.Done()
}()
_, err := client.SendRequest("yes", false, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
ok, err := client.SendRequest("yes", true, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
if !ok {
t.Errorf("SendRequest(yes): %v", ok)
}
ok, err = client.SendRequest("no", true, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
if ok {
t.Errorf("SendRequest(no): %v", ok)
}
client.Close()
wg.Wait()
if received != 3 {
t.Errorf("got %d requests, want %d", received, 3)
}
}
func TestMuxGlobalRequest(t *testing.T) {
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
var seen bool
go func() {
for r := range serverMux.incomingRequests {
seen = seen || r.Type == "peek"
if r.WantReply {
err := r.Reply(r.Type == "yes",
append([]byte(r.Type), r.Payload...))
if err != nil {
t.Errorf("AckRequest: %v", err)
}
}
}
}()
_, _, err := clientMux.SendRequest("peek", false, nil)
if err != nil {
t.Errorf("SendRequest: %v", err)
}
ok, data, err := clientMux.SendRequest("yes", true, []byte("a"))
if !ok || string(data) != "yesa" || err != nil {
t.Errorf("SendRequest(\"yes\", true, \"a\"): %v %v %v",
ok, data, err)
}
if ok, data, err := clientMux.SendRequest("yes", true, []byte("a")); !ok || string(data) != "yesa" || err != nil {
t.Errorf("SendRequest(\"yes\", true, \"a\"): %v %v %v",
ok, data, err)
}
if ok, data, err := clientMux.SendRequest("no", true, []byte("a")); ok || string(data) != "noa" || err != nil {
t.Errorf("SendRequest(\"no\", true, \"a\"): %v %v %v",
ok, data, err)
}
clientMux.Disconnect(0, "")
if !seen {
t.Errorf("never saw 'peek' request")
}
}
func TestMuxGlobalRequestUnblock(t *testing.T) {
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
result := make(chan error, 1)
go func() {
_, _, err := clientMux.SendRequest("hello", true, nil)
result <- err
}()
<-serverMux.incomingRequests
serverMux.conn.Close()
err := <-result
if err != io.EOF {
t.Errorf("want EOF, got %v", io.EOF)
}
}
func TestMuxChannelRequestUnblock(t *testing.T) {
a, b, connB := channelPair(t)
defer a.Close()
defer b.Close()
defer connB.Close()
result := make(chan error, 1)
go func() {
_, err := a.SendRequest("hello", true, nil)
result <- err
}()
<-b.incomingRequests
connB.conn.Close()
err := <-result
if err != io.EOF {
t.Errorf("want EOF, got %v", err)
}
}
func TestMuxDisconnect(t *testing.T) {
a, b := muxPair()
defer a.Close()
defer b.Close()
go func() {
for r := range b.incomingRequests {
r.Reply(true, nil)
}
}()
a.Disconnect(42, "whatever")
ok, _, err := a.SendRequest("hello", true, nil)
if ok || err == nil {
t.Errorf("got reply after disconnecting")
}
err = b.Wait()
if d, ok := err.(*disconnectMsg); !ok || d.Reason != 42 {
t.Errorf("got %#v, want disconnectMsg{Reason:42}", err)
}
}
func TestMuxCloseChannel(t *testing.T) {
r, w, mux := channelPair(t)
defer mux.Close()
defer r.Close()
defer w.Close()
result := make(chan error, 1)
go func() {
var b [1024]byte
_, err := r.Read(b[:])
result <- err
}()
if err := w.Close(); err != nil {
t.Errorf("w.Close: %v", err)
}
if _, err := w.Write([]byte("hello")); err != io.EOF {
t.Errorf("got err %v, want io.EOF after Close", err)
}
if err := <-result; err != io.EOF {
t.Errorf("got %v (%T), want io.EOF", err, err)
}
}
func TestMuxCloseWriteChannel(t *testing.T) {
r, w, mux := channelPair(t)
defer mux.Close()
result := make(chan error, 1)
go func() {
var b [1024]byte
_, err := r.Read(b[:])
result <- err
}()
if err := w.CloseWrite(); err != nil {
t.Errorf("w.CloseWrite: %v", err)
}
if _, err := w.Write([]byte("hello")); err != io.EOF {
t.Errorf("got err %v, want io.EOF after CloseWrite", err)
}
if err := <-result; err != io.EOF {
t.Errorf("got %v (%T), want io.EOF", err, err)
}
}
func TestMuxInvalidRecord(t *testing.T) {
a, b := muxPair()
defer a.Close()
defer b.Close()
packet := make([]byte, 1+4+4+1)
packet[0] = msgChannelData
marshalUint32(packet[1:], 29348723 /* invalid channel id */)
marshalUint32(packet[5:], 1)
packet[9] = 42
a.conn.writePacket(packet)
go a.SendRequest("hello", false, nil)
// 'a' wrote an invalid packet, so 'b' has exited.
req, ok := <-b.incomingRequests
if ok {
t.Errorf("got request %#v after receiving invalid packet", req)
}
}
func TestZeroWindowAdjust(t *testing.T) {
a, b, mux := channelPair(t)
defer a.Close()
defer b.Close()
defer mux.Close()
go func() {
io.WriteString(a, "hello")
// bogus adjust.
a.sendMessage(windowAdjustMsg{})
io.WriteString(a, "world")
a.Close()
}()
want := "helloworld"
c, _ := ioutil.ReadAll(b)
if string(c) != want {
t.Errorf("got %q want %q", c, want)
}
}
func TestMuxMaxPacketSize(t *testing.T) {
a, b, mux := channelPair(t)
defer a.Close()
defer b.Close()
defer mux.Close()
large := make([]byte, a.maxRemotePayload+1)
packet := make([]byte, 1+4+4+1+len(large))
packet[0] = msgChannelData
marshalUint32(packet[1:], a.remoteId)
marshalUint32(packet[5:], uint32(len(large)))
packet[9] = 42
if err := a.mux.conn.writePacket(packet); err != nil {
t.Errorf("could not send packet")
}
go a.SendRequest("hello", false, nil)
_, ok := <-b.incomingRequests
if ok {
t.Errorf("connection still alive after receiving large packet.")
}
}
// Don't ship code with debug=true.
func TestDebug(t *testing.T) {
if debugMux {
t.Error("mux debug switched on")
}
if debugHandshake {
t.Error("handshake debug switched on")
}
}

493
modules/crypto/ssh/server.go Executable file
View File

@@ -0,0 +1,493 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"errors"
"fmt"
"io"
"net"
)
// The Permissions type holds fine-grained permissions that are
// specific to a user or a specific authentication method for a
// user. Permissions, except for "source-address", must be enforced in
// the server application layer, after successful authentication. The
// Permissions are passed on in ServerConn so a server implementation
// can honor them.
type Permissions struct {
// Critical options restrict default permissions. Common
// restrictions are "source-address" and "force-command". If
// the server cannot enforce the restriction, or does not
// recognize it, the user should not authenticate.
CriticalOptions map[string]string
// Extensions are extra functionality that the server may
// offer on authenticated connections. Common extensions are
// "permit-agent-forwarding", "permit-X11-forwarding". Lack of
// support for an extension does not preclude authenticating a
// user.
Extensions map[string]string
}
// ServerConfig holds server specific configuration data.
type ServerConfig struct {
// Config contains configuration shared between client and server.
Config
hostKeys []Signer
// NoClientAuth is true if clients are allowed to connect without
// authenticating.
NoClientAuth bool
// PasswordCallback, if non-nil, is called when a user
// attempts to authenticate using a password.
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
// PublicKeyCallback, if non-nil, is called when a client attempts public
// key authentication. It must return true if the given public key is
// valid for the given user. For example, see CertChecker.Authenticate.
PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
// KeyboardInteractiveCallback, if non-nil, is called when
// keyboard-interactive authentication is selected (RFC
// 4256). The client object's Challenge function should be
// used to query the user. The callback may offer multiple
// Challenge rounds. To avoid information leaks, the client
// should be presented a challenge even if the user is
// unknown.
KeyboardInteractiveCallback func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error)
// AuthLogCallback, if non-nil, is called to log all authentication
// attempts.
AuthLogCallback func(conn ConnMetadata, method string, err error)
// ServerVersion is the version identification string to
// announce in the public handshake.
// If empty, a reasonable default is used.
ServerVersion string
}
// AddHostKey adds a private key as a host key. If an existing host
// key exists with the same algorithm, it is overwritten. Each server
// config must have at least one host key.
func (s *ServerConfig) AddHostKey(key Signer) {
for i, k := range s.hostKeys {
if k.PublicKey().Type() == key.PublicKey().Type() {
s.hostKeys[i] = key
return
}
}
s.hostKeys = append(s.hostKeys, key)
}
// cachedPubKey contains the results of querying whether a public key is
// acceptable for a user.
type cachedPubKey struct {
user string
pubKeyData []byte
result error
perms *Permissions
}
const maxCachedPubKeys = 16
// pubKeyCache caches tests for public keys. Since SSH clients
// will query whether a public key is acceptable before attempting to
// authenticate with it, we end up with duplicate queries for public
// key validity. The cache only applies to a single ServerConn.
type pubKeyCache struct {
keys []cachedPubKey
}
// get returns the result for a given user/algo/key tuple.
func (c *pubKeyCache) get(user string, pubKeyData []byte) (cachedPubKey, bool) {
for _, k := range c.keys {
if k.user == user && bytes.Equal(k.pubKeyData, pubKeyData) {
return k, true
}
}
return cachedPubKey{}, false
}
// add adds the given tuple to the cache.
func (c *pubKeyCache) add(candidate cachedPubKey) {
if len(c.keys) < maxCachedPubKeys {
c.keys = append(c.keys, candidate)
}
}
// ServerConn is an authenticated SSH connection, as seen from the
// server
type ServerConn struct {
Conn
// If the succeeding authentication callback returned a
// non-nil Permissions pointer, it is stored here.
Permissions *Permissions
}
// NewServerConn starts a new SSH server with c as the underlying
// transport. It starts with a handshake and, if the handshake is
// unsuccessful, it closes the connection and returns an error. The
// Request and NewChannel channels must be serviced, or the connection
// will hang.
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
fullConf := *config
fullConf.SetDefaults()
s := &connection{
sshConn: sshConn{conn: c},
}
perms, err := s.serverHandshake(&fullConf)
if err != nil {
c.Close()
return nil, nil, nil, err
}
return &ServerConn{s, perms}, s.mux.incomingChannels, s.mux.incomingRequests, nil
}
// signAndMarshal signs the data with the appropriate algorithm,
// and serializes the result in SSH wire format.
func signAndMarshal(k Signer, rand io.Reader, data []byte) ([]byte, error) {
sig, err := k.Sign(rand, data)
if err != nil {
return nil, err
}
return Marshal(sig), nil
}
// handshake performs key exchange and user authentication.
func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error) {
if len(config.hostKeys) == 0 {
return nil, errors.New("ssh: server has no host keys")
}
if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil && config.KeyboardInteractiveCallback == nil {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
if config.ServerVersion != "" {
s.serverVersion = []byte(config.ServerVersion)
} else {
s.serverVersion = []byte(packageVersion)
}
var err error
s.clientVersion, err = exchangeVersions(s.sshConn.conn, s.serverVersion)
if err != nil {
return nil, err
}
tr := newTransport(s.sshConn.conn, config.Rand, false /* not client */)
s.transport = newServerTransport(tr, s.clientVersion, s.serverVersion, config)
if err := s.transport.requestKeyChange(); err != nil {
return nil, err
}
if packet, err := s.transport.readPacket(); err != nil {
return nil, err
} else if packet[0] != msgNewKeys {
return nil, unexpectedMessageError(msgNewKeys, packet[0])
}
// We just did the key change, so the session ID is established.
s.sessionID = s.transport.getSessionID()
var packet []byte
if packet, err = s.transport.readPacket(); err != nil {
return nil, err
}
var serviceRequest serviceRequestMsg
if err = Unmarshal(packet, &serviceRequest); err != nil {
return nil, err
}
if serviceRequest.Service != serviceUserAuth {
return nil, errors.New("ssh: requested service '" + serviceRequest.Service + "' before authenticating")
}
serviceAccept := serviceAcceptMsg{
Service: serviceUserAuth,
}
if err := s.transport.writePacket(Marshal(&serviceAccept)); err != nil {
return nil, err
}
perms, err := s.serverAuthenticate(config)
if err != nil {
return nil, err
}
s.mux = newMux(s.transport)
return perms, err
}
func isAcceptableAlgo(algo string) bool {
switch algo {
case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01:
return true
}
return false
}
func checkSourceAddress(addr net.Addr, sourceAddr string) error {
if addr == nil {
return errors.New("ssh: no address known for client, but source-address match required")
}
tcpAddr, ok := addr.(*net.TCPAddr)
if !ok {
return fmt.Errorf("ssh: remote address %v is not an TCP address when checking source-address match", addr)
}
if allowedIP := net.ParseIP(sourceAddr); allowedIP != nil {
if bytes.Equal(allowedIP, tcpAddr.IP) {
return nil
}
} else {
_, ipNet, err := net.ParseCIDR(sourceAddr)
if err != nil {
return fmt.Errorf("ssh: error parsing source-address restriction %q: %v", sourceAddr, err)
}
if ipNet.Contains(tcpAddr.IP) {
return nil
}
}
return fmt.Errorf("ssh: remote address %v is not allowed because of source-address restriction", addr)
}
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
var err error
var cache pubKeyCache
var perms *Permissions
userAuthLoop:
for {
var userAuthReq userAuthRequestMsg
if packet, err := s.transport.readPacket(); err != nil {
return nil, err
} else if err = Unmarshal(packet, &userAuthReq); err != nil {
return nil, err
}
if userAuthReq.Service != serviceSSH {
return nil, errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
}
s.user = userAuthReq.User
perms = nil
authErr := errors.New("no auth passed yet")
switch userAuthReq.Method {
case "none":
if config.NoClientAuth {
s.user = ""
authErr = nil
}
case "password":
if config.PasswordCallback == nil {
authErr = errors.New("ssh: password auth not configured")
break
}
payload := userAuthReq.Payload
if len(payload) < 1 || payload[0] != 0 {
return nil, parseError(msgUserAuthRequest)
}
payload = payload[1:]
password, payload, ok := parseString(payload)
if !ok || len(payload) > 0 {
return nil, parseError(msgUserAuthRequest)
}
perms, authErr = config.PasswordCallback(s, password)
case "keyboard-interactive":
if config.KeyboardInteractiveCallback == nil {
authErr = errors.New("ssh: keyboard-interactive auth not configubred")
break
}
prompter := &sshClientKeyboardInteractive{s}
perms, authErr = config.KeyboardInteractiveCallback(s, prompter.Challenge)
case "publickey":
if config.PublicKeyCallback == nil {
authErr = errors.New("ssh: publickey auth not configured")
break
}
payload := userAuthReq.Payload
if len(payload) < 1 {
return nil, parseError(msgUserAuthRequest)
}
isQuery := payload[0] == 0
payload = payload[1:]
algoBytes, payload, ok := parseString(payload)
if !ok {
return nil, parseError(msgUserAuthRequest)
}
algo := string(algoBytes)
if !isAcceptableAlgo(algo) {
authErr = fmt.Errorf("ssh: algorithm %q not accepted", algo)
break
}
pubKeyData, payload, ok := parseString(payload)
if !ok {
return nil, parseError(msgUserAuthRequest)
}
pubKey, err := ParsePublicKey(pubKeyData)
if err != nil {
return nil, err
}
candidate, ok := cache.get(s.user, pubKeyData)
if !ok {
candidate.user = s.user
candidate.pubKeyData = pubKeyData
candidate.perms, candidate.result = config.PublicKeyCallback(s, pubKey)
if candidate.result == nil && candidate.perms != nil && candidate.perms.CriticalOptions != nil && candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
candidate.result = checkSourceAddress(
s.RemoteAddr(),
candidate.perms.CriticalOptions[sourceAddressCriticalOption])
}
cache.add(candidate)
}
if isQuery {
// The client can query if the given public key
// would be okay.
if len(payload) > 0 {
return nil, parseError(msgUserAuthRequest)
}
if candidate.result == nil {
okMsg := userAuthPubKeyOkMsg{
Algo: algo,
PubKey: pubKeyData,
}
if err = s.transport.writePacket(Marshal(&okMsg)); err != nil {
return nil, err
}
continue userAuthLoop
}
authErr = candidate.result
} else {
sig, payload, ok := parseSignature(payload)
if !ok || len(payload) > 0 {
return nil, parseError(msgUserAuthRequest)
}
// Ensure the public key algo and signature algo
// are supported. Compare the private key
// algorithm name that corresponds to algo with
// sig.Format. This is usually the same, but
// for certs, the names differ.
if !isAcceptableAlgo(sig.Format) {
break
}
signedData := buildDataSignedForAuth(s.transport.getSessionID(), userAuthReq, algoBytes, pubKeyData)
if err := pubKey.Verify(signedData, sig); err != nil {
return nil, err
}
authErr = candidate.result
perms = candidate.perms
}
default:
authErr = fmt.Errorf("ssh: unknown method %q", userAuthReq.Method)
}
if config.AuthLogCallback != nil {
config.AuthLogCallback(s, userAuthReq.Method, authErr)
}
if authErr == nil {
break userAuthLoop
}
var failureMsg userAuthFailureMsg
if config.PasswordCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "password")
}
if config.PublicKeyCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "publickey")
}
if config.KeyboardInteractiveCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
}
if len(failureMsg.Methods) == 0 {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
if err = s.transport.writePacket(Marshal(&failureMsg)); err != nil {
return nil, err
}
}
if err = s.transport.writePacket([]byte{msgUserAuthSuccess}); err != nil {
return nil, err
}
return perms, nil
}
// sshClientKeyboardInteractive implements a ClientKeyboardInteractive by
// asking the client on the other side of a ServerConn.
type sshClientKeyboardInteractive struct {
*connection
}
func (c *sshClientKeyboardInteractive) Challenge(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
if len(questions) != len(echos) {
return nil, errors.New("ssh: echos and questions must have equal length")
}
var prompts []byte
for i := range questions {
prompts = appendString(prompts, questions[i])
prompts = appendBool(prompts, echos[i])
}
if err := c.transport.writePacket(Marshal(&userAuthInfoRequestMsg{
Instruction: instruction,
NumPrompts: uint32(len(questions)),
Prompts: prompts,
})); err != nil {
return nil, err
}
packet, err := c.transport.readPacket()
if err != nil {
return nil, err
}
if packet[0] != msgUserAuthInfoResponse {
return nil, unexpectedMessageError(msgUserAuthInfoResponse, packet[0])
}
packet = packet[1:]
n, packet, ok := parseUint32(packet)
if !ok || int(n) != len(questions) {
return nil, parseError(msgUserAuthInfoResponse)
}
for i := uint32(0); i < n; i++ {
ans, rest, ok := parseString(packet)
if !ok {
return nil, parseError(msgUserAuthInfoResponse)
}
answers = append(answers, string(ans))
packet = rest
}
if len(packet) != 0 {
return nil, errors.New("ssh: junk at end of message")
}
return answers, nil
}

605
modules/crypto/ssh/session.go Executable file
View File

@@ -0,0 +1,605 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
// Session implements an interactive session described in
// "RFC 4254, section 6".
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"sync"
)
type Signal string
// POSIX signals as listed in RFC 4254 Section 6.10.
const (
SIGABRT Signal = "ABRT"
SIGALRM Signal = "ALRM"
SIGFPE Signal = "FPE"
SIGHUP Signal = "HUP"
SIGILL Signal = "ILL"
SIGINT Signal = "INT"
SIGKILL Signal = "KILL"
SIGPIPE Signal = "PIPE"
SIGQUIT Signal = "QUIT"
SIGSEGV Signal = "SEGV"
SIGTERM Signal = "TERM"
SIGUSR1 Signal = "USR1"
SIGUSR2 Signal = "USR2"
)
var signals = map[Signal]int{
SIGABRT: 6,
SIGALRM: 14,
SIGFPE: 8,
SIGHUP: 1,
SIGILL: 4,
SIGINT: 2,
SIGKILL: 9,
SIGPIPE: 13,
SIGQUIT: 3,
SIGSEGV: 11,
SIGTERM: 15,
}
type TerminalModes map[uint8]uint32
// POSIX terminal mode flags as listed in RFC 4254 Section 8.
const (
tty_OP_END = 0
VINTR = 1
VQUIT = 2
VERASE = 3
VKILL = 4
VEOF = 5
VEOL = 6
VEOL2 = 7
VSTART = 8
VSTOP = 9
VSUSP = 10
VDSUSP = 11
VREPRINT = 12
VWERASE = 13
VLNEXT = 14
VFLUSH = 15
VSWTCH = 16
VSTATUS = 17
VDISCARD = 18
IGNPAR = 30
PARMRK = 31
INPCK = 32
ISTRIP = 33
INLCR = 34
IGNCR = 35
ICRNL = 36
IUCLC = 37
IXON = 38
IXANY = 39
IXOFF = 40
IMAXBEL = 41
ISIG = 50
ICANON = 51
XCASE = 52
ECHO = 53
ECHOE = 54
ECHOK = 55
ECHONL = 56
NOFLSH = 57
TOSTOP = 58
IEXTEN = 59
ECHOCTL = 60
ECHOKE = 61
PENDIN = 62
OPOST = 70
OLCUC = 71
ONLCR = 72
OCRNL = 73
ONOCR = 74
ONLRET = 75
CS7 = 90
CS8 = 91
PARENB = 92
PARODD = 93
TTY_OP_ISPEED = 128
TTY_OP_OSPEED = 129
)
// A Session represents a connection to a remote command or shell.
type Session struct {
// Stdin specifies the remote process's standard input.
// If Stdin is nil, the remote process reads from an empty
// bytes.Buffer.
Stdin io.Reader
// Stdout and Stderr specify the remote process's standard
// output and error.
//
// If either is nil, Run connects the corresponding file
// descriptor to an instance of ioutil.Discard. There is a
// fixed amount of buffering that is shared for the two streams.
// If either blocks it may eventually cause the remote
// command to block.
Stdout io.Writer
Stderr io.Writer
ch Channel // the channel backing this session
started bool // true once Start, Run or Shell is invoked.
copyFuncs []func() error
errors chan error // one send per copyFunc
// true if pipe method is active
stdinpipe, stdoutpipe, stderrpipe bool
// stdinPipeWriter is non-nil if StdinPipe has not been called
// and Stdin was specified by the user; it is the write end of
// a pipe connecting Session.Stdin to the stdin channel.
stdinPipeWriter io.WriteCloser
exitStatus chan error
}
// SendRequest sends an out-of-band channel request on the SSH channel
// underlying the session.
func (s *Session) SendRequest(name string, wantReply bool, payload []byte) (bool, error) {
return s.ch.SendRequest(name, wantReply, payload)
}
func (s *Session) Close() error {
return s.ch.Close()
}
// RFC 4254 Section 6.4.
type setenvRequest struct {
Name string
Value string
}
// Setenv sets an environment variable that will be applied to any
// command executed by Shell or Run.
func (s *Session) Setenv(name, value string) error {
msg := setenvRequest{
Name: name,
Value: value,
}
ok, err := s.ch.SendRequest("env", true, Marshal(&msg))
if err == nil && !ok {
err = errors.New("ssh: setenv failed")
}
return err
}
// RFC 4254 Section 6.2.
type ptyRequestMsg struct {
Term string
Columns uint32
Rows uint32
Width uint32
Height uint32
Modelist string
}
// RequestPty requests the association of a pty with the session on the remote host.
func (s *Session) RequestPty(term string, h, w int, termmodes TerminalModes) error {
var tm []byte
for k, v := range termmodes {
kv := struct {
Key byte
Val uint32
}{k, v}
tm = append(tm, Marshal(&kv)...)
}
tm = append(tm, tty_OP_END)
req := ptyRequestMsg{
Term: term,
Columns: uint32(w),
Rows: uint32(h),
Width: uint32(w * 8),
Height: uint32(h * 8),
Modelist: string(tm),
}
ok, err := s.ch.SendRequest("pty-req", true, Marshal(&req))
if err == nil && !ok {
err = errors.New("ssh: pty-req failed")
}
return err
}
// RFC 4254 Section 6.5.
type subsystemRequestMsg struct {
Subsystem string
}
// RequestSubsystem requests the association of a subsystem with the session on the remote host.
// A subsystem is a predefined command that runs in the background when the ssh session is initiated
func (s *Session) RequestSubsystem(subsystem string) error {
msg := subsystemRequestMsg{
Subsystem: subsystem,
}
ok, err := s.ch.SendRequest("subsystem", true, Marshal(&msg))
if err == nil && !ok {
err = errors.New("ssh: subsystem request failed")
}
return err
}
// RFC 4254 Section 6.9.
type signalMsg struct {
Signal string
}
// Signal sends the given signal to the remote process.
// sig is one of the SIG* constants.
func (s *Session) Signal(sig Signal) error {
msg := signalMsg{
Signal: string(sig),
}
_, err := s.ch.SendRequest("signal", false, Marshal(&msg))
return err
}
// RFC 4254 Section 6.5.
type execMsg struct {
Command string
}
// Start runs cmd on the remote host. Typically, the remote
// server passes cmd to the shell for interpretation.
// A Session only accepts one call to Run, Start or Shell.
func (s *Session) Start(cmd string) error {
if s.started {
return errors.New("ssh: session already started")
}
req := execMsg{
Command: cmd,
}
ok, err := s.ch.SendRequest("exec", true, Marshal(&req))
if err == nil && !ok {
err = fmt.Errorf("ssh: command %v failed", cmd)
}
if err != nil {
return err
}
return s.start()
}
// Run runs cmd on the remote host. Typically, the remote
// server passes cmd to the shell for interpretation.
// A Session only accepts one call to Run, Start, Shell, Output,
// or CombinedOutput.
//
// The returned error is nil if the command runs, has no problems
// copying stdin, stdout, and stderr, and exits with a zero exit
// status.
//
// If the command fails to run or doesn't complete successfully, the
// error is of type *ExitError. Other error types may be
// returned for I/O problems.
func (s *Session) Run(cmd string) error {
err := s.Start(cmd)
if err != nil {
return err
}
return s.Wait()
}
// Output runs cmd on the remote host and returns its standard output.
func (s *Session) Output(cmd string) ([]byte, error) {
if s.Stdout != nil {
return nil, errors.New("ssh: Stdout already set")
}
var b bytes.Buffer
s.Stdout = &b
err := s.Run(cmd)
return b.Bytes(), err
}
type singleWriter struct {
b bytes.Buffer
mu sync.Mutex
}
func (w *singleWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.b.Write(p)
}
// CombinedOutput runs cmd on the remote host and returns its combined
// standard output and standard error.
func (s *Session) CombinedOutput(cmd string) ([]byte, error) {
if s.Stdout != nil {
return nil, errors.New("ssh: Stdout already set")
}
if s.Stderr != nil {
return nil, errors.New("ssh: Stderr already set")
}
var b singleWriter
s.Stdout = &b
s.Stderr = &b
err := s.Run(cmd)
return b.b.Bytes(), err
}
// Shell starts a login shell on the remote host. A Session only
// accepts one call to Run, Start, Shell, Output, or CombinedOutput.
func (s *Session) Shell() error {
if s.started {
return errors.New("ssh: session already started")
}
ok, err := s.ch.SendRequest("shell", true, nil)
if err == nil && !ok {
return fmt.Errorf("ssh: cound not start shell")
}
if err != nil {
return err
}
return s.start()
}
func (s *Session) start() error {
s.started = true
type F func(*Session)
for _, setupFd := range []F{(*Session).stdin, (*Session).stdout, (*Session).stderr} {
setupFd(s)
}
s.errors = make(chan error, len(s.copyFuncs))
for _, fn := range s.copyFuncs {
go func(fn func() error) {
s.errors <- fn()
}(fn)
}
return nil
}
// Wait waits for the remote command to exit.
//
// The returned error is nil if the command runs, has no problems
// copying stdin, stdout, and stderr, and exits with a zero exit
// status.
//
// If the command fails to run or doesn't complete successfully, the
// error is of type *ExitError. Other error types may be
// returned for I/O problems.
func (s *Session) Wait() error {
if !s.started {
return errors.New("ssh: session not started")
}
waitErr := <-s.exitStatus
if s.stdinPipeWriter != nil {
s.stdinPipeWriter.Close()
}
var copyError error
for _ = range s.copyFuncs {
if err := <-s.errors; err != nil && copyError == nil {
copyError = err
}
}
if waitErr != nil {
return waitErr
}
return copyError
}
func (s *Session) wait(reqs <-chan *Request) error {
wm := Waitmsg{status: -1}
// Wait for msg channel to be closed before returning.
for msg := range reqs {
switch msg.Type {
case "exit-status":
d := msg.Payload
wm.status = int(d[0])<<24 | int(d[1])<<16 | int(d[2])<<8 | int(d[3])
case "exit-signal":
var sigval struct {
Signal string
CoreDumped bool
Error string
Lang string
}
if err := Unmarshal(msg.Payload, &sigval); err != nil {
return err
}
// Must sanitize strings?
wm.signal = sigval.Signal
wm.msg = sigval.Error
wm.lang = sigval.Lang
default:
// This handles keepalives and matches
// OpenSSH's behaviour.
if msg.WantReply {
msg.Reply(false, nil)
}
}
}
if wm.status == 0 {
return nil
}
if wm.status == -1 {
// exit-status was never sent from server
if wm.signal == "" {
return errors.New("wait: remote command exited without exit status or exit signal")
}
wm.status = 128
if _, ok := signals[Signal(wm.signal)]; ok {
wm.status += signals[Signal(wm.signal)]
}
}
return &ExitError{wm}
}
func (s *Session) stdin() {
if s.stdinpipe {
return
}
var stdin io.Reader
if s.Stdin == nil {
stdin = new(bytes.Buffer)
} else {
r, w := io.Pipe()
go func() {
_, err := io.Copy(w, s.Stdin)
w.CloseWithError(err)
}()
stdin, s.stdinPipeWriter = r, w
}
s.copyFuncs = append(s.copyFuncs, func() error {
_, err := io.Copy(s.ch, stdin)
if err1 := s.ch.CloseWrite(); err == nil && err1 != io.EOF {
err = err1
}
return err
})
}
func (s *Session) stdout() {
if s.stdoutpipe {
return
}
if s.Stdout == nil {
s.Stdout = ioutil.Discard
}
s.copyFuncs = append(s.copyFuncs, func() error {
_, err := io.Copy(s.Stdout, s.ch)
return err
})
}
func (s *Session) stderr() {
if s.stderrpipe {
return
}
if s.Stderr == nil {
s.Stderr = ioutil.Discard
}
s.copyFuncs = append(s.copyFuncs, func() error {
_, err := io.Copy(s.Stderr, s.ch.Stderr())
return err
})
}
// sessionStdin reroutes Close to CloseWrite.
type sessionStdin struct {
io.Writer
ch Channel
}
func (s *sessionStdin) Close() error {
return s.ch.CloseWrite()
}
// StdinPipe returns a pipe that will be connected to the
// remote command's standard input when the command starts.
func (s *Session) StdinPipe() (io.WriteCloser, error) {
if s.Stdin != nil {
return nil, errors.New("ssh: Stdin already set")
}
if s.started {
return nil, errors.New("ssh: StdinPipe after process started")
}
s.stdinpipe = true
return &sessionStdin{s.ch, s.ch}, nil
}
// StdoutPipe returns a pipe that will be connected to the
// remote command's standard output when the command starts.
// There is a fixed amount of buffering that is shared between
// stdout and stderr streams. If the StdoutPipe reader is
// not serviced fast enough it may eventually cause the
// remote command to block.
func (s *Session) StdoutPipe() (io.Reader, error) {
if s.Stdout != nil {
return nil, errors.New("ssh: Stdout already set")
}
if s.started {
return nil, errors.New("ssh: StdoutPipe after process started")
}
s.stdoutpipe = true
return s.ch, nil
}
// StderrPipe returns a pipe that will be connected to the
// remote command's standard error when the command starts.
// There is a fixed amount of buffering that is shared between
// stdout and stderr streams. If the StderrPipe reader is
// not serviced fast enough it may eventually cause the
// remote command to block.
func (s *Session) StderrPipe() (io.Reader, error) {
if s.Stderr != nil {
return nil, errors.New("ssh: Stderr already set")
}
if s.started {
return nil, errors.New("ssh: StderrPipe after process started")
}
s.stderrpipe = true
return s.ch.Stderr(), nil
}
// newSession returns a new interactive session on the remote host.
func newSession(ch Channel, reqs <-chan *Request) (*Session, error) {
s := &Session{
ch: ch,
}
s.exitStatus = make(chan error, 1)
go func() {
s.exitStatus <- s.wait(reqs)
}()
return s, nil
}
// An ExitError reports unsuccessful completion of a remote command.
type ExitError struct {
Waitmsg
}
func (e *ExitError) Error() string {
return e.Waitmsg.String()
}
// Waitmsg stores the information about an exited remote command
// as reported by Wait.
type Waitmsg struct {
status int
signal string
msg string
lang string
}
// ExitStatus returns the exit status of the remote command.
func (w Waitmsg) ExitStatus() int {
return w.status
}
// Signal returns the exit signal of the remote command if
// it was terminated violently.
func (w Waitmsg) Signal() string {
return w.signal
}
// Msg returns the exit message given by the remote command
func (w Waitmsg) Msg() string {
return w.msg
}
// Lang returns the language tag. See RFC 3066
func (w Waitmsg) Lang() string {
return w.lang
}
func (w Waitmsg) String() string {
return fmt.Sprintf("Process exited with: %v. Reason was: %v (%v)", w.status, w.msg, w.signal)
}

View File

@@ -0,0 +1,774 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
// Session tests.
import (
"bytes"
crypto_rand "crypto/rand"
"errors"
"io"
"io/ioutil"
"math/rand"
"net"
"testing"
"github.com/gogits/gogs/modules/crypto/ssh/terminal"
)
type serverType func(Channel, <-chan *Request, *testing.T)
// dial constructs a new test server and returns a *ClientConn.
func dial(handler serverType, t *testing.T) *Client {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
go func() {
defer c1.Close()
conf := ServerConfig{
NoClientAuth: true,
}
conf.AddHostKey(testSigners["rsa"])
_, chans, reqs, err := NewServerConn(c1, &conf)
if err != nil {
t.Fatalf("Unable to handshake: %v", err)
}
go DiscardRequests(reqs)
for newCh := range chans {
if newCh.ChannelType() != "session" {
newCh.Reject(UnknownChannelType, "unknown channel type")
continue
}
ch, inReqs, err := newCh.Accept()
if err != nil {
t.Errorf("Accept: %v", err)
continue
}
go func() {
handler(ch, inReqs, t)
}()
}
}()
config := &ClientConfig{
User: "testuser",
}
conn, chans, reqs, err := NewClientConn(c2, "", config)
if err != nil {
t.Fatalf("unable to dial remote side: %v", err)
}
return NewClient(conn, chans, reqs)
}
// Test a simple string is returned to session.Stdout.
func TestSessionShell(t *testing.T) {
conn := dial(shellHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
stdout := new(bytes.Buffer)
session.Stdout = stdout
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %s", err)
}
if err := session.Wait(); err != nil {
t.Fatalf("Remote command did not exit cleanly: %v", err)
}
actual := stdout.String()
if actual != "golang" {
t.Fatalf("Remote shell did not return expected string: expected=golang, actual=%s", actual)
}
}
// TODO(dfc) add support for Std{in,err}Pipe when the Server supports it.
// Test a simple string is returned via StdoutPipe.
func TestSessionStdoutPipe(t *testing.T) {
conn := dial(shellHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
stdout, err := session.StdoutPipe()
if err != nil {
t.Fatalf("Unable to request StdoutPipe(): %v", err)
}
var buf bytes.Buffer
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
done := make(chan bool, 1)
go func() {
if _, err := io.Copy(&buf, stdout); err != nil {
t.Errorf("Copy of stdout failed: %v", err)
}
done <- true
}()
if err := session.Wait(); err != nil {
t.Fatalf("Remote command did not exit cleanly: %v", err)
}
<-done
actual := buf.String()
if actual != "golang" {
t.Fatalf("Remote shell did not return expected string: expected=golang, actual=%s", actual)
}
}
// Test that a simple string is returned via the Output helper,
// and that stderr is discarded.
func TestSessionOutput(t *testing.T) {
conn := dial(fixedOutputHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
buf, err := session.Output("") // cmd is ignored by fixedOutputHandler
if err != nil {
t.Error("Remote command did not exit cleanly:", err)
}
w := "this-is-stdout."
g := string(buf)
if g != w {
t.Error("Remote command did not return expected string:")
t.Logf("want %q", w)
t.Logf("got %q", g)
}
}
// Test that both stdout and stderr are returned
// via the CombinedOutput helper.
func TestSessionCombinedOutput(t *testing.T) {
conn := dial(fixedOutputHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
buf, err := session.CombinedOutput("") // cmd is ignored by fixedOutputHandler
if err != nil {
t.Error("Remote command did not exit cleanly:", err)
}
const stdout = "this-is-stdout."
const stderr = "this-is-stderr."
g := string(buf)
if g != stdout+stderr && g != stderr+stdout {
t.Error("Remote command did not return expected string:")
t.Logf("want %q, or %q", stdout+stderr, stderr+stdout)
t.Logf("got %q", g)
}
}
// Test non-0 exit status is returned correctly.
func TestExitStatusNonZero(t *testing.T) {
conn := dial(exitStatusNonZeroHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
err = session.Wait()
if err == nil {
t.Fatalf("expected command to fail but it didn't")
}
e, ok := err.(*ExitError)
if !ok {
t.Fatalf("expected *ExitError but got %T", err)
}
if e.ExitStatus() != 15 {
t.Fatalf("expected command to exit with 15 but got %v", e.ExitStatus())
}
}
// Test 0 exit status is returned correctly.
func TestExitStatusZero(t *testing.T) {
conn := dial(exitStatusZeroHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
err = session.Wait()
if err != nil {
t.Fatalf("expected nil but got %v", err)
}
}
// Test exit signal and status are both returned correctly.
func TestExitSignalAndStatus(t *testing.T) {
conn := dial(exitSignalAndStatusHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
err = session.Wait()
if err == nil {
t.Fatalf("expected command to fail but it didn't")
}
e, ok := err.(*ExitError)
if !ok {
t.Fatalf("expected *ExitError but got %T", err)
}
if e.Signal() != "TERM" || e.ExitStatus() != 15 {
t.Fatalf("expected command to exit with signal TERM and status 15 but got signal %s and status %v", e.Signal(), e.ExitStatus())
}
}
// Test exit signal and status are both returned correctly.
func TestKnownExitSignalOnly(t *testing.T) {
conn := dial(exitSignalHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
err = session.Wait()
if err == nil {
t.Fatalf("expected command to fail but it didn't")
}
e, ok := err.(*ExitError)
if !ok {
t.Fatalf("expected *ExitError but got %T", err)
}
if e.Signal() != "TERM" || e.ExitStatus() != 143 {
t.Fatalf("expected command to exit with signal TERM and status 143 but got signal %s and status %v", e.Signal(), e.ExitStatus())
}
}
// Test exit signal and status are both returned correctly.
func TestUnknownExitSignal(t *testing.T) {
conn := dial(exitSignalUnknownHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
err = session.Wait()
if err == nil {
t.Fatalf("expected command to fail but it didn't")
}
e, ok := err.(*ExitError)
if !ok {
t.Fatalf("expected *ExitError but got %T", err)
}
if e.Signal() != "SYS" || e.ExitStatus() != 128 {
t.Fatalf("expected command to exit with signal SYS and status 128 but got signal %s and status %v", e.Signal(), e.ExitStatus())
}
}
// Test WaitMsg is not returned if the channel closes abruptly.
func TestExitWithoutStatusOrSignal(t *testing.T) {
conn := dial(exitWithoutSignalOrStatus, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatalf("Unable to request new session: %v", err)
}
defer session.Close()
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
err = session.Wait()
if err == nil {
t.Fatalf("expected command to fail but it didn't")
}
_, ok := err.(*ExitError)
if ok {
// you can't actually test for errors.errorString
// because it's not exported.
t.Fatalf("expected *errorString but got %T", err)
}
}
// windowTestBytes is the number of bytes that we'll send to the SSH server.
const windowTestBytes = 16000 * 200
// TestServerWindow writes random data to the server. The server is expected to echo
// the same data back, which is compared against the original.
func TestServerWindow(t *testing.T) {
origBuf := bytes.NewBuffer(make([]byte, 0, windowTestBytes))
io.CopyN(origBuf, crypto_rand.Reader, windowTestBytes)
origBytes := origBuf.Bytes()
conn := dial(echoHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()
result := make(chan []byte)
go func() {
defer close(result)
echoedBuf := bytes.NewBuffer(make([]byte, 0, windowTestBytes))
serverStdout, err := session.StdoutPipe()
if err != nil {
t.Errorf("StdoutPipe failed: %v", err)
return
}
n, err := copyNRandomly("stdout", echoedBuf, serverStdout, windowTestBytes)
if err != nil && err != io.EOF {
t.Errorf("Read only %d bytes from server, expected %d: %v", n, windowTestBytes, err)
}
result <- echoedBuf.Bytes()
}()
serverStdin, err := session.StdinPipe()
if err != nil {
t.Fatalf("StdinPipe failed: %v", err)
}
written, err := copyNRandomly("stdin", serverStdin, origBuf, windowTestBytes)
if err != nil {
t.Fatalf("failed to copy origBuf to serverStdin: %v", err)
}
if written != windowTestBytes {
t.Fatalf("Wrote only %d of %d bytes to server", written, windowTestBytes)
}
echoedBytes := <-result
if !bytes.Equal(origBytes, echoedBytes) {
t.Fatalf("Echoed buffer differed from original, orig %d, echoed %d", len(origBytes), len(echoedBytes))
}
}
// Verify the client can handle a keepalive packet from the server.
func TestClientHandlesKeepalives(t *testing.T) {
conn := dial(channelKeepaliveSender, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()
if err := session.Shell(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
err = session.Wait()
if err != nil {
t.Fatalf("expected nil but got: %v", err)
}
}
type exitStatusMsg struct {
Status uint32
}
type exitSignalMsg struct {
Signal string
CoreDumped bool
Errmsg string
Lang string
}
func handleTerminalRequests(in <-chan *Request) {
for req := range in {
ok := false
switch req.Type {
case "shell":
ok = true
if len(req.Payload) > 0 {
// We don't accept any commands, only the default shell.
ok = false
}
case "env":
ok = true
}
req.Reply(ok, nil)
}
}
func newServerShell(ch Channel, in <-chan *Request, prompt string) *terminal.Terminal {
term := terminal.NewTerminal(ch, prompt)
go handleTerminalRequests(in)
return term
}
func exitStatusZeroHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
// this string is returned to stdout
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
sendStatus(0, ch, t)
}
func exitStatusNonZeroHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
sendStatus(15, ch, t)
}
func exitSignalAndStatusHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
sendStatus(15, ch, t)
sendSignal("TERM", ch, t)
}
func exitSignalHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
sendSignal("TERM", ch, t)
}
func exitSignalUnknownHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
sendSignal("SYS", ch, t)
}
func exitWithoutSignalOrStatus(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
}
func shellHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
// this string is returned to stdout
shell := newServerShell(ch, in, "golang")
readLine(shell, t)
sendStatus(0, ch, t)
}
// Ignores the command, writes fixed strings to stderr and stdout.
// Strings are "this-is-stdout." and "this-is-stderr.".
func fixedOutputHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
_, err := ch.Read(nil)
req, ok := <-in
if !ok {
t.Fatalf("error: expected channel request, got: %#v", err)
return
}
// ignore request, always send some text
req.Reply(true, nil)
_, err = io.WriteString(ch, "this-is-stdout.")
if err != nil {
t.Fatalf("error writing on server: %v", err)
}
_, err = io.WriteString(ch.Stderr(), "this-is-stderr.")
if err != nil {
t.Fatalf("error writing on server: %v", err)
}
sendStatus(0, ch, t)
}
func readLine(shell *terminal.Terminal, t *testing.T) {
if _, err := shell.ReadLine(); err != nil && err != io.EOF {
t.Errorf("unable to read line: %v", err)
}
}
func sendStatus(status uint32, ch Channel, t *testing.T) {
msg := exitStatusMsg{
Status: status,
}
if _, err := ch.SendRequest("exit-status", false, Marshal(&msg)); err != nil {
t.Errorf("unable to send status: %v", err)
}
}
func sendSignal(signal string, ch Channel, t *testing.T) {
sig := exitSignalMsg{
Signal: signal,
CoreDumped: false,
Errmsg: "Process terminated",
Lang: "en-GB-oed",
}
if _, err := ch.SendRequest("exit-signal", false, Marshal(&sig)); err != nil {
t.Errorf("unable to send signal: %v", err)
}
}
func discardHandler(ch Channel, t *testing.T) {
defer ch.Close()
io.Copy(ioutil.Discard, ch)
}
func echoHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
if n, err := copyNRandomly("echohandler", ch, ch, windowTestBytes); err != nil {
t.Errorf("short write, wrote %d, expected %d: %v ", n, windowTestBytes, err)
}
}
// copyNRandomly copies n bytes from src to dst. It uses a variable, and random,
// buffer size to exercise more code paths.
func copyNRandomly(title string, dst io.Writer, src io.Reader, n int) (int, error) {
var (
buf = make([]byte, 32*1024)
written int
remaining = n
)
for remaining > 0 {
l := rand.Intn(1 << 15)
if remaining < l {
l = remaining
}
nr, er := src.Read(buf[:l])
nw, ew := dst.Write(buf[:nr])
remaining -= nw
written += nw
if ew != nil {
return written, ew
}
if nr != nw {
return written, io.ErrShortWrite
}
if er != nil && er != io.EOF {
return written, er
}
}
return written, nil
}
func channelKeepaliveSender(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
if _, err := ch.SendRequest("keepalive@openssh.com", true, nil); err != nil {
t.Errorf("unable to send channel keepalive request: %v", err)
}
sendStatus(0, ch, t)
}
func TestClientWriteEOF(t *testing.T) {
conn := dial(simpleEchoHandler, t)
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()
stdin, err := session.StdinPipe()
if err != nil {
t.Fatalf("StdinPipe failed: %v", err)
}
stdout, err := session.StdoutPipe()
if err != nil {
t.Fatalf("StdoutPipe failed: %v", err)
}
data := []byte(`0000`)
_, err = stdin.Write(data)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
stdin.Close()
res, err := ioutil.ReadAll(stdout)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
if !bytes.Equal(data, res) {
t.Fatalf("Read differed from write, wrote: %v, read: %v", data, res)
}
}
func simpleEchoHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
data, err := ioutil.ReadAll(ch)
if err != nil {
t.Errorf("handler read error: %v", err)
}
_, err = ch.Write(data)
if err != nil {
t.Errorf("handler write error: %v", err)
}
}
func TestSessionID(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
serverID := make(chan []byte, 1)
clientID := make(chan []byte, 1)
serverConf := &ServerConfig{
NoClientAuth: true,
}
serverConf.AddHostKey(testSigners["ecdsa"])
clientConf := &ClientConfig{
User: "user",
}
go func() {
conn, chans, reqs, err := NewServerConn(c1, serverConf)
if err != nil {
t.Fatalf("server handshake: %v", err)
}
serverID <- conn.SessionID()
go DiscardRequests(reqs)
for ch := range chans {
ch.Reject(Prohibited, "")
}
}()
go func() {
conn, chans, reqs, err := NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
clientID <- conn.SessionID()
go DiscardRequests(reqs)
for ch := range chans {
ch.Reject(Prohibited, "")
}
}()
s := <-serverID
c := <-clientID
if bytes.Compare(s, c) != 0 {
t.Errorf("server session ID (%x) != client session ID (%x)", s, c)
} else if len(s) == 0 {
t.Errorf("client and server SessionID were empty.")
}
}
type noReadConn struct {
readSeen bool
net.Conn
}
func (c *noReadConn) Close() error {
return nil
}
func (c *noReadConn) Read(b []byte) (int, error) {
c.readSeen = true
return 0, errors.New("noReadConn error")
}
func TestInvalidServerConfiguration(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
serveConn := noReadConn{Conn: c1}
serverConf := &ServerConfig{}
NewServerConn(&serveConn, serverConf)
if serveConn.readSeen {
t.Fatalf("NewServerConn attempted to Read() from Conn while configuration is missing host key")
}
serverConf.AddHostKey(testSigners["ecdsa"])
NewServerConn(&serveConn, serverConf)
if serveConn.readSeen {
t.Fatalf("NewServerConn attempted to Read() from Conn while configuration is missing authentication method")
}
}
func TestHostKeyAlgorithms(t *testing.T) {
serverConf := &ServerConfig{
NoClientAuth: true,
}
serverConf.AddHostKey(testSigners["rsa"])
serverConf.AddHostKey(testSigners["ecdsa"])
connect := func(clientConf *ClientConfig, want string) {
var alg string
clientConf.HostKeyCallback = func(h string, a net.Addr, key PublicKey) error {
alg = key.Type()
return nil
}
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
go NewServerConn(c1, serverConf)
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("NewClientConn: %v", err)
}
if alg != want {
t.Errorf("selected key algorithm %s, want %s", alg, want)
}
}
// By default, we get the preferred algorithm, which is ECDSA 256.
clientConf := &ClientConfig{}
connect(clientConf, KeyAlgoECDSA256)
// Client asks for RSA explicitly.
clientConf.HostKeyAlgorithms = []string{KeyAlgoRSA}
connect(clientConf, KeyAlgoRSA)
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
go NewServerConn(c1, serverConf)
clientConf.HostKeyAlgorithms = []string{"nonexistent-hostkey-algo"}
_, _, _, err = NewClientConn(c2, "", clientConf)
if err == nil {
t.Fatal("succeeded connecting with unknown hostkey algorithm")
}
}

407
modules/crypto/ssh/tcpip.go Executable file
View File

@@ -0,0 +1,407 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"errors"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
)
// Listen requests the remote peer open a listening socket on
// addr. Incoming connections will be available by calling Accept on
// the returned net.Listener. The listener must be serviced, or the
// SSH connection may hang.
func (c *Client) Listen(n, addr string) (net.Listener, error) {
laddr, err := net.ResolveTCPAddr(n, addr)
if err != nil {
return nil, err
}
return c.ListenTCP(laddr)
}
// Automatic port allocation is broken with OpenSSH before 6.0. See
// also https://bugzilla.mindrot.org/show_bug.cgi?id=2017. In
// particular, OpenSSH 5.9 sends a channelOpenMsg with port number 0,
// rather than the actual port number. This means you can never open
// two different listeners with auto allocated ports. We work around
// this by trying explicit ports until we succeed.
const openSSHPrefix = "OpenSSH_"
var portRandomizer = rand.New(rand.NewSource(time.Now().UnixNano()))
// isBrokenOpenSSHVersion returns true if the given version string
// specifies a version of OpenSSH that is known to have a bug in port
// forwarding.
func isBrokenOpenSSHVersion(versionStr string) bool {
i := strings.Index(versionStr, openSSHPrefix)
if i < 0 {
return false
}
i += len(openSSHPrefix)
j := i
for ; j < len(versionStr); j++ {
if versionStr[j] < '0' || versionStr[j] > '9' {
break
}
}
version, _ := strconv.Atoi(versionStr[i:j])
return version < 6
}
// autoPortListenWorkaround simulates automatic port allocation by
// trying random ports repeatedly.
func (c *Client) autoPortListenWorkaround(laddr *net.TCPAddr) (net.Listener, error) {
var sshListener net.Listener
var err error
const tries = 10
for i := 0; i < tries; i++ {
addr := *laddr
addr.Port = 1024 + portRandomizer.Intn(60000)
sshListener, err = c.ListenTCP(&addr)
if err == nil {
laddr.Port = addr.Port
return sshListener, err
}
}
return nil, fmt.Errorf("ssh: listen on random port failed after %d tries: %v", tries, err)
}
// RFC 4254 7.1
type channelForwardMsg struct {
addr string
rport uint32
}
// ListenTCP requests the remote peer open a listening socket
// on laddr. Incoming connections will be available by calling
// Accept on the returned net.Listener.
func (c *Client) ListenTCP(laddr *net.TCPAddr) (net.Listener, error) {
if laddr.Port == 0 && isBrokenOpenSSHVersion(string(c.ServerVersion())) {
return c.autoPortListenWorkaround(laddr)
}
m := channelForwardMsg{
laddr.IP.String(),
uint32(laddr.Port),
}
// send message
ok, resp, err := c.SendRequest("tcpip-forward", true, Marshal(&m))
if err != nil {
return nil, err
}
if !ok {
return nil, errors.New("ssh: tcpip-forward request denied by peer")
}
// If the original port was 0, then the remote side will
// supply a real port number in the response.
if laddr.Port == 0 {
var p struct {
Port uint32
}
if err := Unmarshal(resp, &p); err != nil {
return nil, err
}
laddr.Port = int(p.Port)
}
// Register this forward, using the port number we obtained.
ch := c.forwards.add(*laddr)
return &tcpListener{laddr, c, ch}, nil
}
// forwardList stores a mapping between remote
// forward requests and the tcpListeners.
type forwardList struct {
sync.Mutex
entries []forwardEntry
}
// forwardEntry represents an established mapping of a laddr on a
// remote ssh server to a channel connected to a tcpListener.
type forwardEntry struct {
laddr net.TCPAddr
c chan forward
}
// forward represents an incoming forwarded tcpip connection. The
// arguments to add/remove/lookup should be address as specified in
// the original forward-request.
type forward struct {
newCh NewChannel // the ssh client channel underlying this forward
raddr *net.TCPAddr // the raddr of the incoming connection
}
func (l *forwardList) add(addr net.TCPAddr) chan forward {
l.Lock()
defer l.Unlock()
f := forwardEntry{
addr,
make(chan forward, 1),
}
l.entries = append(l.entries, f)
return f.c
}
// See RFC 4254, section 7.2
type forwardedTCPPayload struct {
Addr string
Port uint32
OriginAddr string
OriginPort uint32
}
// parseTCPAddr parses the originating address from the remote into a *net.TCPAddr.
func parseTCPAddr(addr string, port uint32) (*net.TCPAddr, error) {
if port == 0 || port > 65535 {
return nil, fmt.Errorf("ssh: port number out of range: %d", port)
}
ip := net.ParseIP(string(addr))
if ip == nil {
return nil, fmt.Errorf("ssh: cannot parse IP address %q", addr)
}
return &net.TCPAddr{IP: ip, Port: int(port)}, nil
}
func (l *forwardList) handleChannels(in <-chan NewChannel) {
for ch := range in {
var payload forwardedTCPPayload
if err := Unmarshal(ch.ExtraData(), &payload); err != nil {
ch.Reject(ConnectionFailed, "could not parse forwarded-tcpip payload: "+err.Error())
continue
}
// RFC 4254 section 7.2 specifies that incoming
// addresses should list the address, in string
// format. It is implied that this should be an IP
// address, as it would be impossible to connect to it
// otherwise.
laddr, err := parseTCPAddr(payload.Addr, payload.Port)
if err != nil {
ch.Reject(ConnectionFailed, err.Error())
continue
}
raddr, err := parseTCPAddr(payload.OriginAddr, payload.OriginPort)
if err != nil {
ch.Reject(ConnectionFailed, err.Error())
continue
}
if ok := l.forward(*laddr, *raddr, ch); !ok {
// Section 7.2, implementations MUST reject spurious incoming
// connections.
ch.Reject(Prohibited, "no forward for address")
continue
}
}
}
// remove removes the forward entry, and the channel feeding its
// listener.
func (l *forwardList) remove(addr net.TCPAddr) {
l.Lock()
defer l.Unlock()
for i, f := range l.entries {
if addr.IP.Equal(f.laddr.IP) && addr.Port == f.laddr.Port {
l.entries = append(l.entries[:i], l.entries[i+1:]...)
close(f.c)
return
}
}
}
// closeAll closes and clears all forwards.
func (l *forwardList) closeAll() {
l.Lock()
defer l.Unlock()
for _, f := range l.entries {
close(f.c)
}
l.entries = nil
}
func (l *forwardList) forward(laddr, raddr net.TCPAddr, ch NewChannel) bool {
l.Lock()
defer l.Unlock()
for _, f := range l.entries {
if laddr.IP.Equal(f.laddr.IP) && laddr.Port == f.laddr.Port {
f.c <- forward{ch, &raddr}
return true
}
}
return false
}
type tcpListener struct {
laddr *net.TCPAddr
conn *Client
in <-chan forward
}
// Accept waits for and returns the next connection to the listener.
func (l *tcpListener) Accept() (net.Conn, error) {
s, ok := <-l.in
if !ok {
return nil, io.EOF
}
ch, incoming, err := s.newCh.Accept()
if err != nil {
return nil, err
}
go DiscardRequests(incoming)
return &tcpChanConn{
Channel: ch,
laddr: l.laddr,
raddr: s.raddr,
}, nil
}
// Close closes the listener.
func (l *tcpListener) Close() error {
m := channelForwardMsg{
l.laddr.IP.String(),
uint32(l.laddr.Port),
}
// this also closes the listener.
l.conn.forwards.remove(*l.laddr)
ok, _, err := l.conn.SendRequest("cancel-tcpip-forward", true, Marshal(&m))
if err == nil && !ok {
err = errors.New("ssh: cancel-tcpip-forward failed")
}
return err
}
// Addr returns the listener's network address.
func (l *tcpListener) Addr() net.Addr {
return l.laddr
}
// Dial initiates a connection to the addr from the remote host.
// The resulting connection has a zero LocalAddr() and RemoteAddr().
func (c *Client) Dial(n, addr string) (net.Conn, error) {
// Parse the address into host and numeric port.
host, portString, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
port, err := strconv.ParseUint(portString, 10, 16)
if err != nil {
return nil, err
}
// Use a zero address for local and remote address.
zeroAddr := &net.TCPAddr{
IP: net.IPv4zero,
Port: 0,
}
ch, err := c.dial(net.IPv4zero.String(), 0, host, int(port))
if err != nil {
return nil, err
}
return &tcpChanConn{
Channel: ch,
laddr: zeroAddr,
raddr: zeroAddr,
}, nil
}
// DialTCP connects to the remote address raddr on the network net,
// which must be "tcp", "tcp4", or "tcp6". If laddr is not nil, it is used
// as the local address for the connection.
func (c *Client) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error) {
if laddr == nil {
laddr = &net.TCPAddr{
IP: net.IPv4zero,
Port: 0,
}
}
ch, err := c.dial(laddr.IP.String(), laddr.Port, raddr.IP.String(), raddr.Port)
if err != nil {
return nil, err
}
return &tcpChanConn{
Channel: ch,
laddr: laddr,
raddr: raddr,
}, nil
}
// RFC 4254 7.2
type channelOpenDirectMsg struct {
raddr string
rport uint32
laddr string
lport uint32
}
func (c *Client) dial(laddr string, lport int, raddr string, rport int) (Channel, error) {
msg := channelOpenDirectMsg{
raddr: raddr,
rport: uint32(rport),
laddr: laddr,
lport: uint32(lport),
}
ch, in, err := c.OpenChannel("direct-tcpip", Marshal(&msg))
if err != nil {
return nil, err
}
go DiscardRequests(in)
return ch, err
}
type tcpChan struct {
Channel // the backing channel
}
// tcpChanConn fulfills the net.Conn interface without
// the tcpChan having to hold laddr or raddr directly.
type tcpChanConn struct {
Channel
laddr, raddr net.Addr
}
// LocalAddr returns the local network address.
func (t *tcpChanConn) LocalAddr() net.Addr {
return t.laddr
}
// RemoteAddr returns the remote network address.
func (t *tcpChanConn) RemoteAddr() net.Addr {
return t.raddr
}
// SetDeadline sets the read and write deadlines associated
// with the connection.
func (t *tcpChanConn) SetDeadline(deadline time.Time) error {
if err := t.SetReadDeadline(deadline); err != nil {
return err
}
return t.SetWriteDeadline(deadline)
}
// SetReadDeadline sets the read deadline.
// A zero value for t means Read will not time out.
// After the deadline, the error from Read will implement net.Error
// with Timeout() == true.
func (t *tcpChanConn) SetReadDeadline(deadline time.Time) error {
return errors.New("ssh: tcpChan: deadline not supported")
}
// SetWriteDeadline exists to satisfy the net.Conn interface
// but is not implemented by this type. It always returns an error.
func (t *tcpChanConn) SetWriteDeadline(deadline time.Time) error {
return errors.New("ssh: tcpChan: deadline not supported")
}

View File

@@ -0,0 +1,20 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"testing"
)
func TestAutoPortListenBroken(t *testing.T) {
broken := "SSH-2.0-OpenSSH_5.9hh11"
works := "SSH-2.0-OpenSSH_6.1"
if !isBrokenOpenSSHVersion(broken) {
t.Errorf("version %q not marked as broken", broken)
}
if isBrokenOpenSSHVersion(works) {
t.Errorf("version %q marked as broken", works)
}
}

View File

@@ -0,0 +1,892 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package terminal
import (
"bytes"
"io"
"sync"
"unicode/utf8"
)
// EscapeCodes contains escape sequences that can be written to the terminal in
// order to achieve different styles of text.
type EscapeCodes struct {
// Foreground colors
Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte
// Reset all attributes
Reset []byte
}
var vt100EscapeCodes = EscapeCodes{
Black: []byte{keyEscape, '[', '3', '0', 'm'},
Red: []byte{keyEscape, '[', '3', '1', 'm'},
Green: []byte{keyEscape, '[', '3', '2', 'm'},
Yellow: []byte{keyEscape, '[', '3', '3', 'm'},
Blue: []byte{keyEscape, '[', '3', '4', 'm'},
Magenta: []byte{keyEscape, '[', '3', '5', 'm'},
Cyan: []byte{keyEscape, '[', '3', '6', 'm'},
White: []byte{keyEscape, '[', '3', '7', 'm'},
Reset: []byte{keyEscape, '[', '0', 'm'},
}
// Terminal contains the state for running a VT100 terminal that is capable of
// reading lines of input.
type Terminal struct {
// AutoCompleteCallback, if non-null, is called for each keypress with
// the full input line and the current position of the cursor (in
// bytes, as an index into |line|). If it returns ok=false, the key
// press is processed normally. Otherwise it returns a replacement line
// and the new cursor position.
AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool)
// Escape contains a pointer to the escape codes for this terminal.
// It's always a valid pointer, although the escape codes themselves
// may be empty if the terminal doesn't support them.
Escape *EscapeCodes
// lock protects the terminal and the state in this object from
// concurrent processing of a key press and a Write() call.
lock sync.Mutex
c io.ReadWriter
prompt []rune
// line is the current line being entered.
line []rune
// pos is the logical position of the cursor in line
pos int
// echo is true if local echo is enabled
echo bool
// pasteActive is true iff there is a bracketed paste operation in
// progress.
pasteActive bool
// cursorX contains the current X value of the cursor where the left
// edge is 0. cursorY contains the row number where the first row of
// the current line is 0.
cursorX, cursorY int
// maxLine is the greatest value of cursorY so far.
maxLine int
termWidth, termHeight int
// outBuf contains the terminal data to be sent.
outBuf []byte
// remainder contains the remainder of any partial key sequences after
// a read. It aliases into inBuf.
remainder []byte
inBuf [256]byte
// history contains previously entered commands so that they can be
// accessed with the up and down keys.
history stRingBuffer
// historyIndex stores the currently accessed history entry, where zero
// means the immediately previous entry.
historyIndex int
// When navigating up and down the history it's possible to return to
// the incomplete, initial line. That value is stored in
// historyPending.
historyPending string
}
// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
// a local terminal, that terminal must first have been put into raw mode.
// prompt is a string that is written at the start of each input line (i.e.
// "> ").
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
return &Terminal{
Escape: &vt100EscapeCodes,
c: c,
prompt: []rune(prompt),
termWidth: 80,
termHeight: 24,
echo: true,
historyIndex: -1,
}
}
const (
keyCtrlD = 4
keyCtrlU = 21
keyEnter = '\r'
keyEscape = 27
keyBackspace = 127
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota
keyUp
keyDown
keyLeft
keyRight
keyAltLeft
keyAltRight
keyHome
keyEnd
keyDeleteWord
keyDeleteLine
keyClearScreen
keyPasteStart
keyPasteEnd
)
var pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'}
var pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'}
// bytesToKey tries to parse a key sequence from b. If successful, it returns
// the key and the remainder of the input. Otherwise it returns utf8.RuneError.
func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
if len(b) == 0 {
return utf8.RuneError, nil
}
if !pasteActive {
switch b[0] {
case 1: // ^A
return keyHome, b[1:]
case 5: // ^E
return keyEnd, b[1:]
case 8: // ^H
return keyBackspace, b[1:]
case 11: // ^K
return keyDeleteLine, b[1:]
case 12: // ^L
return keyClearScreen, b[1:]
case 23: // ^W
return keyDeleteWord, b[1:]
}
}
if b[0] != keyEscape {
if !utf8.FullRune(b) {
return utf8.RuneError, b
}
r, l := utf8.DecodeRune(b)
return r, b[l:]
}
if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' {
switch b[2] {
case 'A':
return keyUp, b[3:]
case 'B':
return keyDown, b[3:]
case 'C':
return keyRight, b[3:]
case 'D':
return keyLeft, b[3:]
case 'H':
return keyHome, b[3:]
case 'F':
return keyEnd, b[3:]
}
}
if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
switch b[5] {
case 'C':
return keyAltRight, b[6:]
case 'D':
return keyAltLeft, b[6:]
}
}
if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) {
return keyPasteStart, b[6:]
}
if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) {
return keyPasteEnd, b[6:]
}
// If we get here then we have a key that we don't recognise, or a
// partial sequence. It's not clear how one should find the end of a
// sequence without knowing them all, but it seems that [a-zA-Z~] only
// appears at the end of a sequence.
for i, c := range b[0:] {
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' {
return keyUnknown, b[i+1:]
}
}
return utf8.RuneError, b
}
// queue appends data to the end of t.outBuf
func (t *Terminal) queue(data []rune) {
t.outBuf = append(t.outBuf, []byte(string(data))...)
}
var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'}
var space = []rune{' '}
func isPrintable(key rune) bool {
isInSurrogateArea := key >= 0xd800 && key <= 0xdbff
return key >= 32 && !isInSurrogateArea
}
// moveCursorToPos appends data to t.outBuf which will move the cursor to the
// given, logical position in the text.
func (t *Terminal) moveCursorToPos(pos int) {
if !t.echo {
return
}
x := visualLength(t.prompt) + pos
y := x / t.termWidth
x = x % t.termWidth
up := 0
if y < t.cursorY {
up = t.cursorY - y
}
down := 0
if y > t.cursorY {
down = y - t.cursorY
}
left := 0
if x < t.cursorX {
left = t.cursorX - x
}
right := 0
if x > t.cursorX {
right = x - t.cursorX
}
t.cursorX = x
t.cursorY = y
t.move(up, down, left, right)
}
func (t *Terminal) move(up, down, left, right int) {
movement := make([]rune, 3*(up+down+left+right))
m := movement
for i := 0; i < up; i++ {
m[0] = keyEscape
m[1] = '['
m[2] = 'A'
m = m[3:]
}
for i := 0; i < down; i++ {
m[0] = keyEscape
m[1] = '['
m[2] = 'B'
m = m[3:]
}
for i := 0; i < left; i++ {
m[0] = keyEscape
m[1] = '['
m[2] = 'D'
m = m[3:]
}
for i := 0; i < right; i++ {
m[0] = keyEscape
m[1] = '['
m[2] = 'C'
m = m[3:]
}
t.queue(movement)
}
func (t *Terminal) clearLineToRight() {
op := []rune{keyEscape, '[', 'K'}
t.queue(op)
}
const maxLineLength = 4096
func (t *Terminal) setLine(newLine []rune, newPos int) {
if t.echo {
t.moveCursorToPos(0)
t.writeLine(newLine)
for i := len(newLine); i < len(t.line); i++ {
t.writeLine(space)
}
t.moveCursorToPos(newPos)
}
t.line = newLine
t.pos = newPos
}
func (t *Terminal) advanceCursor(places int) {
t.cursorX += places
t.cursorY += t.cursorX / t.termWidth
if t.cursorY > t.maxLine {
t.maxLine = t.cursorY
}
t.cursorX = t.cursorX % t.termWidth
if places > 0 && t.cursorX == 0 {
// Normally terminals will advance the current position
// when writing a character. But that doesn't happen
// for the last character in a line. However, when
// writing a character (except a new line) that causes
// a line wrap, the position will be advanced two
// places.
//
// So, if we are stopping at the end of a line, we
// need to write a newline so that our cursor can be
// advanced to the next line.
t.outBuf = append(t.outBuf, '\n')
}
}
func (t *Terminal) eraseNPreviousChars(n int) {
if n == 0 {
return
}
if t.pos < n {
n = t.pos
}
t.pos -= n
t.moveCursorToPos(t.pos)
copy(t.line[t.pos:], t.line[n+t.pos:])
t.line = t.line[:len(t.line)-n]
if t.echo {
t.writeLine(t.line[t.pos:])
for i := 0; i < n; i++ {
t.queue(space)
}
t.advanceCursor(n)
t.moveCursorToPos(t.pos)
}
}
// countToLeftWord returns then number of characters from the cursor to the
// start of the previous word.
func (t *Terminal) countToLeftWord() int {
if t.pos == 0 {
return 0
}
pos := t.pos - 1
for pos > 0 {
if t.line[pos] != ' ' {
break
}
pos--
}
for pos > 0 {
if t.line[pos] == ' ' {
pos++
break
}
pos--
}
return t.pos - pos
}
// countToRightWord returns then number of characters from the cursor to the
// start of the next word.
func (t *Terminal) countToRightWord() int {
pos := t.pos
for pos < len(t.line) {
if t.line[pos] == ' ' {
break
}
pos++
}
for pos < len(t.line) {
if t.line[pos] != ' ' {
break
}
pos++
}
return pos - t.pos
}
// visualLength returns the number of visible glyphs in s.
func visualLength(runes []rune) int {
inEscapeSeq := false
length := 0
for _, r := range runes {
switch {
case inEscapeSeq:
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
inEscapeSeq = false
}
case r == '\x1b':
inEscapeSeq = true
default:
length++
}
}
return length
}
// handleKey processes the given key and, optionally, returns a line of text
// that the user has entered.
func (t *Terminal) handleKey(key rune) (line string, ok bool) {
if t.pasteActive && key != keyEnter {
t.addKeyToLine(key)
return
}
switch key {
case keyBackspace:
if t.pos == 0 {
return
}
t.eraseNPreviousChars(1)
case keyAltLeft:
// move left by a word.
t.pos -= t.countToLeftWord()
t.moveCursorToPos(t.pos)
case keyAltRight:
// move right by a word.
t.pos += t.countToRightWord()
t.moveCursorToPos(t.pos)
case keyLeft:
if t.pos == 0 {
return
}
t.pos--
t.moveCursorToPos(t.pos)
case keyRight:
if t.pos == len(t.line) {
return
}
t.pos++
t.moveCursorToPos(t.pos)
case keyHome:
if t.pos == 0 {
return
}
t.pos = 0
t.moveCursorToPos(t.pos)
case keyEnd:
if t.pos == len(t.line) {
return
}
t.pos = len(t.line)
t.moveCursorToPos(t.pos)
case keyUp:
entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1)
if !ok {
return "", false
}
if t.historyIndex == -1 {
t.historyPending = string(t.line)
}
t.historyIndex++
runes := []rune(entry)
t.setLine(runes, len(runes))
case keyDown:
switch t.historyIndex {
case -1:
return
case 0:
runes := []rune(t.historyPending)
t.setLine(runes, len(runes))
t.historyIndex--
default:
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1)
if ok {
t.historyIndex--
runes := []rune(entry)
t.setLine(runes, len(runes))
}
}
case keyEnter:
t.moveCursorToPos(len(t.line))
t.queue([]rune("\r\n"))
line = string(t.line)
ok = true
t.line = t.line[:0]
t.pos = 0
t.cursorX = 0
t.cursorY = 0
t.maxLine = 0
case keyDeleteWord:
// Delete zero or more spaces and then one or more characters.
t.eraseNPreviousChars(t.countToLeftWord())
case keyDeleteLine:
// Delete everything from the current cursor position to the
// end of line.
for i := t.pos; i < len(t.line); i++ {
t.queue(space)
t.advanceCursor(1)
}
t.line = t.line[:t.pos]
t.moveCursorToPos(t.pos)
case keyCtrlD:
// Erase the character under the current position.
// The EOF case when the line is empty is handled in
// readLine().
if t.pos < len(t.line) {
t.pos++
t.eraseNPreviousChars(1)
}
case keyCtrlU:
t.eraseNPreviousChars(t.pos)
case keyClearScreen:
// Erases the screen and moves the cursor to the home position.
t.queue([]rune("\x1b[2J\x1b[H"))
t.queue(t.prompt)
t.cursorX, t.cursorY = 0, 0
t.advanceCursor(visualLength(t.prompt))
t.setLine(t.line, t.pos)
default:
if t.AutoCompleteCallback != nil {
prefix := string(t.line[:t.pos])
suffix := string(t.line[t.pos:])
t.lock.Unlock()
newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key)
t.lock.Lock()
if completeOk {
t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos]))
return
}
}
if !isPrintable(key) {
return
}
if len(t.line) == maxLineLength {
return
}
t.addKeyToLine(key)
}
return
}
// addKeyToLine inserts the given key at the current position in the current
// line.
func (t *Terminal) addKeyToLine(key rune) {
if len(t.line) == cap(t.line) {
newLine := make([]rune, len(t.line), 2*(1+len(t.line)))
copy(newLine, t.line)
t.line = newLine
}
t.line = t.line[:len(t.line)+1]
copy(t.line[t.pos+1:], t.line[t.pos:])
t.line[t.pos] = key
if t.echo {
t.writeLine(t.line[t.pos:])
}
t.pos++
t.moveCursorToPos(t.pos)
}
func (t *Terminal) writeLine(line []rune) {
for len(line) != 0 {
remainingOnLine := t.termWidth - t.cursorX
todo := len(line)
if todo > remainingOnLine {
todo = remainingOnLine
}
t.queue(line[:todo])
t.advanceCursor(visualLength(line[:todo]))
line = line[todo:]
}
}
func (t *Terminal) Write(buf []byte) (n int, err error) {
t.lock.Lock()
defer t.lock.Unlock()
if t.cursorX == 0 && t.cursorY == 0 {
// This is the easy case: there's nothing on the screen that we
// have to move out of the way.
return t.c.Write(buf)
}
// We have a prompt and possibly user input on the screen. We
// have to clear it first.
t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */)
t.cursorX = 0
t.clearLineToRight()
for t.cursorY > 0 {
t.move(1 /* up */, 0, 0, 0)
t.cursorY--
t.clearLineToRight()
}
if _, err = t.c.Write(t.outBuf); err != nil {
return
}
t.outBuf = t.outBuf[:0]
if n, err = t.c.Write(buf); err != nil {
return
}
t.writeLine(t.prompt)
if t.echo {
t.writeLine(t.line)
}
t.moveCursorToPos(t.pos)
if _, err = t.c.Write(t.outBuf); err != nil {
return
}
t.outBuf = t.outBuf[:0]
return
}
// ReadPassword temporarily changes the prompt and reads a password, without
// echo, from the terminal.
func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
t.lock.Lock()
defer t.lock.Unlock()
oldPrompt := t.prompt
t.prompt = []rune(prompt)
t.echo = false
line, err = t.readLine()
t.prompt = oldPrompt
t.echo = true
return
}
// ReadLine returns a line of input from the terminal.
func (t *Terminal) ReadLine() (line string, err error) {
t.lock.Lock()
defer t.lock.Unlock()
return t.readLine()
}
func (t *Terminal) readLine() (line string, err error) {
// t.lock must be held at this point
if t.cursorX == 0 && t.cursorY == 0 {
t.writeLine(t.prompt)
t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0]
}
lineIsPasted := t.pasteActive
for {
rest := t.remainder
lineOk := false
for !lineOk {
var key rune
key, rest = bytesToKey(rest, t.pasteActive)
if key == utf8.RuneError {
break
}
if !t.pasteActive {
if key == keyCtrlD {
if len(t.line) == 0 {
return "", io.EOF
}
}
if key == keyPasteStart {
t.pasteActive = true
if len(t.line) == 0 {
lineIsPasted = true
}
continue
}
} else if key == keyPasteEnd {
t.pasteActive = false
continue
}
if !t.pasteActive {
lineIsPasted = false
}
line, lineOk = t.handleKey(key)
}
if len(rest) > 0 {
n := copy(t.inBuf[:], rest)
t.remainder = t.inBuf[:n]
} else {
t.remainder = nil
}
t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0]
if lineOk {
if t.echo {
t.historyIndex = -1
t.history.Add(line)
}
if lineIsPasted {
err = ErrPasteIndicator
}
return
}
// t.remainder is a slice at the beginning of t.inBuf
// containing a partial key sequence
readBuf := t.inBuf[len(t.remainder):]
var n int
t.lock.Unlock()
n, err = t.c.Read(readBuf)
t.lock.Lock()
if err != nil {
return
}
t.remainder = t.inBuf[:n+len(t.remainder)]
}
panic("unreachable") // for Go 1.0.
}
// SetPrompt sets the prompt to be used when reading subsequent lines.
func (t *Terminal) SetPrompt(prompt string) {
t.lock.Lock()
defer t.lock.Unlock()
t.prompt = []rune(prompt)
}
func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) {
// Move cursor to column zero at the start of the line.
t.move(t.cursorY, 0, t.cursorX, 0)
t.cursorX, t.cursorY = 0, 0
t.clearLineToRight()
for t.cursorY < numPrevLines {
// Move down a line
t.move(0, 1, 0, 0)
t.cursorY++
t.clearLineToRight()
}
// Move back to beginning.
t.move(t.cursorY, 0, 0, 0)
t.cursorX, t.cursorY = 0, 0
t.queue(t.prompt)
t.advanceCursor(visualLength(t.prompt))
t.writeLine(t.line)
t.moveCursorToPos(t.pos)
}
func (t *Terminal) SetSize(width, height int) error {
t.lock.Lock()
defer t.lock.Unlock()
if width == 0 {
width = 1
}
oldWidth := t.termWidth
t.termWidth, t.termHeight = width, height
switch {
case width == oldWidth:
// If the width didn't change then nothing else needs to be
// done.
return nil
case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0:
// If there is nothing on current line and no prompt printed,
// just do nothing
return nil
case width < oldWidth:
// Some terminals (e.g. xterm) will truncate lines that were
// too long when shinking. Others, (e.g. gnome-terminal) will
// attempt to wrap them. For the former, repainting t.maxLine
// works great, but that behaviour goes badly wrong in the case
// of the latter because they have doubled every full line.
// We assume that we are working on a terminal that wraps lines
// and adjust the cursor position based on every previous line
// wrapping and turning into two. This causes the prompt on
// xterms to move upwards, which isn't great, but it avoids a
// huge mess with gnome-terminal.
if t.cursorX >= t.termWidth {
t.cursorX = t.termWidth - 1
}
t.cursorY *= 2
t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2)
case width > oldWidth:
// If the terminal expands then our position calculations will
// be wrong in the future because we think the cursor is
// |t.pos| chars into the string, but there will be a gap at
// the end of any wrapped line.
//
// But the position will actually be correct until we move, so
// we can move back to the beginning and repaint everything.
t.clearAndRepaintLinePlusNPrevious(t.maxLine)
}
_, err := t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0]
return err
}
type pasteIndicatorError struct{}
func (pasteIndicatorError) Error() string {
return "terminal: ErrPasteIndicator not correctly handled"
}
// ErrPasteIndicator may be returned from ReadLine as the error, in addition
// to valid line data. It indicates that bracketed paste mode is enabled and
// that the returned line consists only of pasted data. Programs may wish to
// interpret pasted data more literally than typed data.
var ErrPasteIndicator = pasteIndicatorError{}
// SetBracketedPasteMode requests that the terminal bracket paste operations
// with markers. Not all terminals support this but, if it is supported, then
// enabling this mode will stop any autocomplete callback from running due to
// pastes. Additionally, any lines that are completely pasted will be returned
// from ReadLine with the error set to ErrPasteIndicator.
func (t *Terminal) SetBracketedPasteMode(on bool) {
if on {
io.WriteString(t.c, "\x1b[?2004h")
} else {
io.WriteString(t.c, "\x1b[?2004l")
}
}
// stRingBuffer is a ring buffer of strings.
type stRingBuffer struct {
// entries contains max elements.
entries []string
max int
// head contains the index of the element most recently added to the ring.
head int
// size contains the number of elements in the ring.
size int
}
func (s *stRingBuffer) Add(a string) {
if s.entries == nil {
const defaultNumEntries = 100
s.entries = make([]string, defaultNumEntries)
s.max = defaultNumEntries
}
s.head = (s.head + 1) % s.max
s.entries[s.head] = a
if s.size < s.max {
s.size++
}
}
// NthPreviousEntry returns the value passed to the nth previous call to Add.
// If n is zero then the immediately prior value is returned, if one, then the
// next most recent, and so on. If such an element doesn't exist then ok is
// false.
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) {
if n >= s.size {
return "", false
}
index := s.head - n
if index < 0 {
index += s.max
}
return s.entries[index], true
}

View File

@@ -0,0 +1,269 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package terminal
import (
"io"
"testing"
)
type MockTerminal struct {
toSend []byte
bytesPerRead int
received []byte
}
func (c *MockTerminal) Read(data []byte) (n int, err error) {
n = len(data)
if n == 0 {
return
}
if n > len(c.toSend) {
n = len(c.toSend)
}
if n == 0 {
return 0, io.EOF
}
if c.bytesPerRead > 0 && n > c.bytesPerRead {
n = c.bytesPerRead
}
copy(data, c.toSend[:n])
c.toSend = c.toSend[n:]
return
}
func (c *MockTerminal) Write(data []byte) (n int, err error) {
c.received = append(c.received, data...)
return len(data), nil
}
func TestClose(t *testing.T) {
c := &MockTerminal{}
ss := NewTerminal(c, "> ")
line, err := ss.ReadLine()
if line != "" {
t.Errorf("Expected empty line but got: %s", line)
}
if err != io.EOF {
t.Errorf("Error should have been EOF but got: %s", err)
}
}
var keyPressTests = []struct {
in string
line string
err error
throwAwayLines int
}{
{
err: io.EOF,
},
{
in: "\r",
line: "",
},
{
in: "foo\r",
line: "foo",
},
{
in: "a\x1b[Cb\r", // right
line: "ab",
},
{
in: "a\x1b[Db\r", // left
line: "ba",
},
{
in: "a\177b\r", // backspace
line: "b",
},
{
in: "\x1b[A\r", // up
},
{
in: "\x1b[B\r", // down
},
{
in: "line\x1b[A\x1b[B\r", // up then down
line: "line",
},
{
in: "line1\rline2\x1b[A\r", // recall previous line.
line: "line1",
throwAwayLines: 1,
},
{
// recall two previous lines and append.
in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r",
line: "line1xxx",
throwAwayLines: 2,
},
{
// Ctrl-A to move to beginning of line followed by ^K to kill
// line.
in: "a b \001\013\r",
line: "",
},
{
// Ctrl-A to move to beginning of line, Ctrl-E to move to end,
// finally ^K to kill nothing.
in: "a b \001\005\013\r",
line: "a b ",
},
{
in: "\027\r",
line: "",
},
{
in: "a\027\r",
line: "",
},
{
in: "a \027\r",
line: "",
},
{
in: "a b\027\r",
line: "a ",
},
{
in: "a b \027\r",
line: "a ",
},
{
in: "one two thr\x1b[D\027\r",
line: "one two r",
},
{
in: "\013\r",
line: "",
},
{
in: "a\013\r",
line: "a",
},
{
in: "ab\x1b[D\013\r",
line: "a",
},
{
in: "Ξεσκεπάζω\r",
line: "Ξεσκεπάζω",
},
{
in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace.
line: "",
throwAwayLines: 1,
},
{
in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter.
line: "£",
throwAwayLines: 1,
},
{
// Ctrl-D at the end of the line should be ignored.
in: "a\004\r",
line: "a",
},
{
// a, b, left, Ctrl-D should erase the b.
in: "ab\x1b[D\004\r",
line: "a",
},
{
// a, b, c, d, left, left, ^U should erase to the beginning of
// the line.
in: "abcd\x1b[D\x1b[D\025\r",
line: "cd",
},
{
// Bracketed paste mode: control sequences should be returned
// verbatim in paste mode.
in: "abc\x1b[200~de\177f\x1b[201~\177\r",
line: "abcde\177",
},
{
// Enter in bracketed paste mode should still work.
in: "abc\x1b[200~d\refg\x1b[201~h\r",
line: "efgh",
throwAwayLines: 1,
},
{
// Lines consisting entirely of pasted data should be indicated as such.
in: "\x1b[200~a\r",
line: "a",
err: ErrPasteIndicator,
},
}
func TestKeyPresses(t *testing.T) {
for i, test := range keyPressTests {
for j := 1; j < len(test.in); j++ {
c := &MockTerminal{
toSend: []byte(test.in),
bytesPerRead: j,
}
ss := NewTerminal(c, "> ")
for k := 0; k < test.throwAwayLines; k++ {
_, err := ss.ReadLine()
if err != nil {
t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err)
}
}
line, err := ss.ReadLine()
if line != test.line {
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
break
}
if err != test.err {
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
break
}
}
}
}
func TestPasswordNotSaved(t *testing.T) {
c := &MockTerminal{
toSend: []byte("password\r\x1b[A\r"),
bytesPerRead: 1,
}
ss := NewTerminal(c, "> ")
pw, _ := ss.ReadPassword("> ")
if pw != "password" {
t.Fatalf("failed to read password, got %s", pw)
}
line, _ := ss.ReadLine()
if len(line) > 0 {
t.Fatalf("password was saved in history")
}
}
var setSizeTests = []struct {
width, height int
}{
{40, 13},
{80, 24},
{132, 43},
}
func TestTerminalSetSize(t *testing.T) {
for _, setSize := range setSizeTests {
c := &MockTerminal{
toSend: []byte("password\r\x1b[A\r"),
bytesPerRead: 1,
}
ss := NewTerminal(c, "> ")
ss.SetSize(setSize.width, setSize.height)
pw, _ := ss.ReadPassword("Password: ")
if pw != "password" {
t.Fatalf("failed to read password, got %s", pw)
}
if string(c.received) != "Password: \r\n" {
t.Errorf("failed to set the temporary prompt expected %q, got %q", "Password: ", c.received)
}
}
}

View File

@@ -0,0 +1,128 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd
// Package terminal provides support functions for dealing with terminals, as
// commonly found on UNIX systems.
//
// Putting a terminal into raw mode is the most common requirement:
//
// oldState, err := terminal.MakeRaw(0)
// if err != nil {
// panic(err)
// }
// defer terminal.Restore(0, oldState)
package terminal
import (
"io"
"syscall"
"unsafe"
)
// State contains the state of a terminal.
type State struct {
termios syscall.Termios
}
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(fd int) bool {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}
// MakeRaw put the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
func MakeRaw(fd int) (*State, error) {
var oldState State
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 {
return nil, err
}
newState := oldState.termios
newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF
newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
return nil, err
}
return &oldState, nil
}
// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
func GetState(fd int) (*State, error) {
var oldState State
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 {
return nil, err
}
return &oldState, nil
}
// Restore restores the terminal connected to the given file descriptor to a
// previous state.
func Restore(fd int, state *State) error {
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0)
return err
}
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (width, height int, err error) {
var dimensions [4]uint16
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 {
return -1, -1, err
}
return int(dimensions[1]), int(dimensions[0]), nil
}
// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
func ReadPassword(fd int) ([]byte, error) {
var oldState syscall.Termios
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 {
return nil, err
}
newState := oldState
newState.Lflag &^= syscall.ECHO
newState.Lflag |= syscall.ICANON | syscall.ISIG
newState.Iflag |= syscall.ICRNL
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
return nil, err
}
defer func() {
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0)
}()
var buf [16]byte
var ret []byte
for {
n, err := syscall.Read(fd, buf[:])
if err != nil {
return nil, err
}
if n == 0 {
if len(ret) == 0 {
return nil, io.EOF
}
break
}
if buf[n-1] == '\n' {
n--
}
ret = append(ret, buf[:n]...)
if n < len(buf) {
break
}
}
return ret, nil
}

View File

@@ -0,0 +1,12 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd netbsd openbsd
package terminal
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
const ioctlWriteTermios = syscall.TIOCSETA

View File

@@ -0,0 +1,11 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package terminal
// These constants are declared here, rather than importing
// them from the syscall package as some syscall packages, even
// on linux, for example gccgo, do not declare them.
const ioctlReadTermios = 0x5401 // syscall.TCGETS
const ioctlWriteTermios = 0x5402 // syscall.TCSETS

View File

@@ -0,0 +1,174 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
// Package terminal provides support functions for dealing with terminals, as
// commonly found on UNIX systems.
//
// Putting a terminal into raw mode is the most common requirement:
//
// oldState, err := terminal.MakeRaw(0)
// if err != nil {
// panic(err)
// }
// defer terminal.Restore(0, oldState)
package terminal
import (
"io"
"syscall"
"unsafe"
)
const (
enableLineInput = 2
enableEchoInput = 4
enableProcessedInput = 1
enableWindowInput = 8
enableMouseInput = 16
enableInsertMode = 32
enableQuickEditMode = 64
enableExtendedFlags = 128
enableAutoPosition = 256
enableProcessedOutput = 1
enableWrapAtEolOutput = 2
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
)
type (
short int16
word uint16
coord struct {
x short
y short
}
smallRect struct {
left short
top short
right short
bottom short
}
consoleScreenBufferInfo struct {
size coord
cursorPosition coord
attributes word
window smallRect
maximumWindowSize coord
}
)
type State struct {
mode uint32
}
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(fd int) bool {
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}
// MakeRaw put the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
func MakeRaw(fd int) (*State, error) {
var st uint32
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
if e != 0 {
return nil, error(e)
}
st &^= (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput)
_, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0)
if e != 0 {
return nil, error(e)
}
return &State{st}, nil
}
// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
func GetState(fd int) (*State, error) {
var st uint32
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
if e != 0 {
return nil, error(e)
}
return &State{st}, nil
}
// Restore restores the terminal connected to the given file descriptor to a
// previous state.
func Restore(fd int, state *State) error {
_, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0)
return err
}
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (width, height int, err error) {
var info consoleScreenBufferInfo
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0)
if e != 0 {
return 0, 0, error(e)
}
return int(info.size.x), int(info.size.y), nil
}
// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
func ReadPassword(fd int) ([]byte, error) {
var st uint32
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
if e != 0 {
return nil, error(e)
}
old := st
st &^= (enableEchoInput)
st |= (enableProcessedInput | enableLineInput | enableProcessedOutput)
_, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0)
if e != 0 {
return nil, error(e)
}
defer func() {
syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0)
}()
var buf [16]byte
var ret []byte
for {
n, err := syscall.Read(syscall.Handle(fd), buf[:])
if err != nil {
return nil, err
}
if n == 0 {
if len(ret) == 0 {
return nil, io.EOF
}
break
}
if buf[n-1] == '\n' {
n--
}
if n > 0 && buf[n-1] == '\r' {
n--
}
ret = append(ret, buf[:n]...)
if n < len(buf) {
break
}
}
return ret, nil
}

View File

@@ -0,0 +1,59 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd linux netbsd openbsd
package test
import (
"bytes"
"testing"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
func TestAgentForward(t *testing.T) {
server := newServer(t)
defer server.Shutdown()
conn := server.Dial(clientConfig())
defer conn.Close()
keyring := agent.NewKeyring()
if err := keyring.Add(agent.AddedKey{PrivateKey: testPrivateKeys["dsa"]}); err != nil {
t.Fatalf("Error adding key: %s", err)
}
if err := keyring.Add(agent.AddedKey{
PrivateKey: testPrivateKeys["dsa"],
ConfirmBeforeUse: true,
LifetimeSecs: 3600,
}); err != nil {
t.Fatalf("Error adding key with constraints: %s", err)
}
pub := testPublicKeys["dsa"]
sess, err := conn.NewSession()
if err != nil {
t.Fatalf("NewSession: %v", err)
}
if err := agent.RequestAgentForwarding(sess); err != nil {
t.Fatalf("RequestAgentForwarding: %v", err)
}
if err := agent.ForwardToAgent(conn, keyring); err != nil {
t.Fatalf("SetupForwardKeyring: %v", err)
}
out, err := sess.CombinedOutput("ssh-add -L")
if err != nil {
t.Fatalf("running ssh-add: %v, out %s", err, out)
}
key, _, _, _, err := ssh.ParseAuthorizedKey(out)
if err != nil {
t.Fatalf("ParseAuthorizedKey(%q): %v", out, err)
}
if !bytes.Equal(key.Marshal(), pub.Marshal()) {
t.Fatalf("got key %s, want %s", ssh.MarshalAuthorizedKey(key), ssh.MarshalAuthorizedKey(pub))
}
}

View File

@@ -0,0 +1,47 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd linux netbsd openbsd
package test
import (
"crypto/rand"
"testing"
"golang.org/x/crypto/ssh"
)
func TestCertLogin(t *testing.T) {
s := newServer(t)
defer s.Shutdown()
// Use a key different from the default.
clientKey := testSigners["dsa"]
caAuthKey := testSigners["ecdsa"]
cert := &ssh.Certificate{
Key: clientKey.PublicKey(),
ValidPrincipals: []string{username()},
CertType: ssh.UserCert,
ValidBefore: ssh.CertTimeInfinity,
}
if err := cert.SignCert(rand.Reader, caAuthKey); err != nil {
t.Fatalf("SetSignature: %v", err)
}
certSigner, err := ssh.NewCertSigner(cert, clientKey)
if err != nil {
t.Fatalf("NewCertSigner: %v", err)
}
conf := &ssh.ClientConfig{
User: username(),
}
conf.Auth = append(conf.Auth, ssh.PublicKeys(certSigner))
client, err := s.TryDial(conf)
if err != nil {
t.Fatalf("TryDial: %v", err)
}
client.Close()
}

7
modules/crypto/ssh/test/doc.go Executable file
View File

@@ -0,0 +1,7 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This package contains integration tests for the
// golang.org/x/crypto/ssh package.
package test

View File

@@ -0,0 +1,160 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd linux netbsd openbsd
package test
import (
"bytes"
"io"
"io/ioutil"
"math/rand"
"net"
"testing"
"time"
)
func TestPortForward(t *testing.T) {
server := newServer(t)
defer server.Shutdown()
conn := server.Dial(clientConfig())
defer conn.Close()
sshListener, err := conn.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
go func() {
sshConn, err := sshListener.Accept()
if err != nil {
t.Fatalf("listen.Accept failed: %v", err)
}
_, err = io.Copy(sshConn, sshConn)
if err != nil && err != io.EOF {
t.Fatalf("ssh client copy: %v", err)
}
sshConn.Close()
}()
forwardedAddr := sshListener.Addr().String()
tcpConn, err := net.Dial("tcp", forwardedAddr)
if err != nil {
t.Fatalf("TCP dial failed: %v", err)
}
readChan := make(chan []byte)
go func() {
data, _ := ioutil.ReadAll(tcpConn)
readChan <- data
}()
// Invent some data.
data := make([]byte, 100*1000)
for i := range data {
data[i] = byte(i % 255)
}
var sent []byte
for len(sent) < 1000*1000 {
// Send random sized chunks
m := rand.Intn(len(data))
n, err := tcpConn.Write(data[:m])
if err != nil {
break
}
sent = append(sent, data[:n]...)
}
if err := tcpConn.(*net.TCPConn).CloseWrite(); err != nil {
t.Errorf("tcpConn.CloseWrite: %v", err)
}
read := <-readChan
if len(sent) != len(read) {
t.Fatalf("got %d bytes, want %d", len(read), len(sent))
}
if bytes.Compare(sent, read) != 0 {
t.Fatalf("read back data does not match")
}
if err := sshListener.Close(); err != nil {
t.Fatalf("sshListener.Close: %v", err)
}
// Check that the forward disappeared.
tcpConn, err = net.Dial("tcp", forwardedAddr)
if err == nil {
tcpConn.Close()
t.Errorf("still listening to %s after closing", forwardedAddr)
}
}
func TestAcceptClose(t *testing.T) {
server := newServer(t)
defer server.Shutdown()
conn := server.Dial(clientConfig())
sshListener, err := conn.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
quit := make(chan error, 1)
go func() {
for {
c, err := sshListener.Accept()
if err != nil {
quit <- err
break
}
c.Close()
}
}()
sshListener.Close()
select {
case <-time.After(1 * time.Second):
t.Errorf("timeout: listener did not close.")
case err := <-quit:
t.Logf("quit as expected (error %v)", err)
}
}
// Check that listeners exit if the underlying client transport dies.
func TestPortForwardConnectionClose(t *testing.T) {
server := newServer(t)
defer server.Shutdown()
conn := server.Dial(clientConfig())
sshListener, err := conn.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
quit := make(chan error, 1)
go func() {
for {
c, err := sshListener.Accept()
if err != nil {
quit <- err
break
}
c.Close()
}
}()
// It would be even nicer if we closed the server side, but it
// is more involved as the fd for that side is dup()ed.
server.clientConn.Close()
select {
case <-time.After(1 * time.Second):
t.Errorf("timeout: listener did not close.")
case err := <-quit:
t.Logf("quit as expected (error %v)", err)
}
}

Some files were not shown because too many files have changed in this diff Show More