Latest develop updates is merged with my RaspberryPi Dockerfile version.
Merge branch 'develop' of https://github.com/gogits/gogs into develop
This commit is contained in:
commit
737da1a374
|
@ -13,7 +13,7 @@ watch_dirs = [
|
||||||
watch_exts = [".go"]
|
watch_exts = [".go"]
|
||||||
build_delay = 1500
|
build_delay = 1500
|
||||||
cmds = [
|
cmds = [
|
||||||
["go", "install", "-tags", "sqlite"],# redis memcache cert pam tidb
|
["go", "install", "-race"], # sqlite redis memcache cert pam tidb
|
||||||
["go", "build", "-tags", "sqlite"],
|
["go", "build", "-race"],
|
||||||
["./gogs", "web"]
|
["./gogs", "web"]
|
||||||
]
|
]
|
|
@ -36,3 +36,4 @@ docker/docker/init_gogs.sh
|
||||||
gogs.sublime-project
|
gogs.sublime-project
|
||||||
gogs.sublime-workspace
|
gogs.sublime-workspace
|
||||||
.tags*
|
.tags*
|
||||||
|
release
|
||||||
|
|
61
.gopmfile
61
.gopmfile
|
@ -3,42 +3,43 @@ path = github.com/gogits/gogs
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
github.com/bradfitz/gomemcache = commit:72a68649ba
|
github.com/bradfitz/gomemcache = commit:72a68649ba
|
||||||
github.com/Unknwon/cae = commit:2e70a1351b
|
github.com/codegangsta/cli = commit:0302d39
|
||||||
github.com/Unknwon/com = commit:47d7d2b81a
|
github.com/go-macaron/binding = commit:864a5ce
|
||||||
github.com/Unknwon/i18n = commit:7457d88830
|
github.com/go-macaron/cache = commit:5617353
|
||||||
github.com/Unknwon/paginater = commit:cab2d086fa
|
github.com/go-macaron/captcha = commit:875ff77
|
||||||
github.com/codegangsta/cli = commit:142e6cd241
|
github.com/go-macaron/csrf = commit:3372b25
|
||||||
github.com/go-sql-driver/mysql = commit:527bcd55aa
|
github.com/go-macaron/gzip = commit:4938e9b
|
||||||
|
github.com/go-macaron/i18n = commit:5e728b6
|
||||||
|
github.com/go-macaron/inject = commit:c5ab7bf
|
||||||
|
github.com/go-macaron/session = commit:66031fc
|
||||||
|
github.com/go-macaron/toolbox = commit:ab30a81
|
||||||
|
github.com/go-sql-driver/mysql = commit:d512f20
|
||||||
github.com/go-xorm/core = commit:3e10003353
|
github.com/go-xorm/core = commit:3e10003353
|
||||||
github.com/go-xorm/xorm = commit:803f6db50c
|
github.com/go-xorm/xorm = commit:c643188
|
||||||
github.com/gogits/chardet = commit:2404f77725
|
github.com/gogits/chardet = commit:2404f77725
|
||||||
github.com/gogits/go-gogs-client = commit:519eee0af0
|
github.com/gogits/go-gogs-client = commit:7c02c95
|
||||||
github.com/issue9/identicon =
|
github.com/issue9/identicon = commit:5a61672
|
||||||
github.com/lib/pq = commit:b269bd035a
|
github.com/klauspost/compress = commit:bbfa9dc
|
||||||
github.com/go-macaron/binding =
|
github.com/klauspost/cpuid = commit:8d9fe96
|
||||||
github.com/go-macaron/cache =
|
github.com/klauspost/crc32 = commit:3e5c38b
|
||||||
github.com/go-macaron/captcha =
|
github.com/lib/pq = commit:83c4f41
|
||||||
github.com/go-macaron/csrf =
|
github.com/mattn/go-sqlite3 = commit:5651a9d
|
||||||
github.com/go-macaron/gzip =
|
github.com/mcuadros/go-version = commit:d52711f
|
||||||
github.com/go-macaron/i18n =
|
github.com/microcosm-cc/bluemonday = commit:4ac6f27
|
||||||
github.com/go-macaron/session =
|
|
||||||
github.com/go-macaron/toolbox =
|
|
||||||
github.com/klauspost/compress =
|
|
||||||
github.com/klauspost/crc32 =
|
|
||||||
github.com/klauspost/cpuid =
|
|
||||||
github.com/mattn/go-sqlite3 = commit:b808f01f66
|
|
||||||
github.com/mcuadros/go-version = commit:d52711f8d6
|
|
||||||
github.com/microcosm-cc/bluemonday = commit:85ba47ef2c
|
|
||||||
github.com/mssola/user_agent = commit:a163d6a569
|
|
||||||
github.com/msteinert/pam = commit:6534f23b39
|
github.com/msteinert/pam = commit:6534f23b39
|
||||||
github.com/nfnt/resize = commit:dc93e1b98c
|
github.com/nfnt/resize = commit:dc93e1b98c
|
||||||
github.com/russross/blackfriday = commit:8cec3a854e
|
github.com/russross/blackfriday = commit:300106c
|
||||||
github.com/shurcooL/sanitized_anchor_name = commit:244f5ac324
|
github.com/shurcooL/sanitized_anchor_name = commit:10ef21a
|
||||||
|
github.com/Unknwon/cae = commit:7f5e046
|
||||||
|
github.com/Unknwon/com = commit:28b053d
|
||||||
|
github.com/Unknwon/i18n = commit:7457d88830
|
||||||
|
github.com/Unknwon/paginater = commit:7748a72
|
||||||
golang.org/x/net =
|
golang.org/x/net =
|
||||||
golang.org/x/text =
|
golang.org/x/text =
|
||||||
gopkg.in/gomail.v2 = commit:b1e55520bf
|
golang.org/x/crypto =
|
||||||
gopkg.in/macaron.v1 =
|
gopkg.in/gomail.v2 = commit:df6fc79
|
||||||
gopkg.in/ini.v1 = commit:e8c222fea7
|
gopkg.in/ini.v1 = commit:2e44421
|
||||||
|
gopkg.in/macaron.v1 = commit:1c6dd87
|
||||||
gopkg.in/redis.v2 = commit:e617904962
|
gopkg.in/redis.v2 = commit:e617904962
|
||||||
|
|
||||||
[res]
|
[res]
|
||||||
|
|
|
@ -4,13 +4,15 @@ go:
|
||||||
- 1.3
|
- 1.3
|
||||||
- 1.4
|
- 1.4
|
||||||
- 1.5
|
- 1.5
|
||||||
- tip
|
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- sudo apt-get update -qq
|
- sudo apt-get update -qq
|
||||||
- sudo apt-get install -y libpam-dev
|
- sudo apt-get install -y libpam-dev
|
||||||
- go get github.com/msteinert/pam
|
- go get github.com/msteinert/pam
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get -t -v ./...
|
||||||
|
|
||||||
script: go build -v -tags "pam"
|
script: go build -v -tags "pam"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
|
|
|
@ -19,4 +19,4 @@ RUN ./docker/build.sh
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
EXPOSE 22 3000
|
EXPOSE 22 3000
|
||||||
ENTRYPOINT ["docker/start.sh"]
|
ENTRYPOINT ["docker/start.sh"]
|
||||||
CMD ["/usr/bin/s6-svscan", "/app/gogs/docker/s6/"]
|
CMD ["/bin/s6-svscan", "/app/gogs/docker/s6/"]
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
LDFLAGS += -X "github.com/gogits/gogs/modules/setting.BuildTime=$(shell date -u '+%Y-%m-%d %I:%M:%S %Z')"
|
||||||
|
LDFLAGS += -X "github.com/gogits/gogs/modules/setting.BuildGitHash=$(shell git rev-parse HEAD)"
|
||||||
|
|
||||||
|
TAGS = ""
|
||||||
|
|
||||||
|
RELEASE_ROOT = "release"
|
||||||
|
RELEASE_GOGS = "release/gogs"
|
||||||
|
NOW = $(shell date -u '+%Y%m%d%I%M%S')
|
||||||
|
|
||||||
|
.PHONY: build pack release bindata clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
go install -ldflags '$(LDFLAGS)' -tags '$(TAGS)'
|
||||||
|
go build -ldflags '$(LDFLAGS)' -tags '$(TAGS)'
|
||||||
|
|
||||||
|
govet:
|
||||||
|
go tool vet -composites=false -methods=false -structtags=false .
|
||||||
|
|
||||||
|
pack:
|
||||||
|
rm -rf $(RELEASE_GOGS)
|
||||||
|
mkdir -p $(RELEASE_GOGS)
|
||||||
|
cp -r gogs LICENSE README.md README_ZH.md templates public scripts $(RELEASE_GOGS)
|
||||||
|
rm -rf $(RELEASE_GOGS)/public/config.codekit $(RELEASE_GOGS)/public/less
|
||||||
|
cd $(RELEASE_ROOT) && zip -r gogs.$(NOW).zip "gogs"
|
||||||
|
|
||||||
|
release: build pack
|
||||||
|
|
||||||
|
bindata:
|
||||||
|
go-bindata -o=modules/bindata/bindata.go -ignore="\\.DS_Store|README.md" -pkg=bindata conf/...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean -i ./...
|
||||||
|
|
||||||
|
clean-mac: clean
|
||||||
|
find . -name ".DS_Store" -print0 | xargs -0 rm
|
39
README.md
39
README.md
|
@ -5,23 +5,23 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra
|
||||||
|
|
||||||
![](public/img/gogs-large-resize.png)
|
![](public/img/gogs-large-resize.png)
|
||||||
|
|
||||||
##### Current version: 0.6.18 Beta
|
##### Current version: 0.7.20 Beta
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="33%"><img src="http://gogs.io/img/screenshots/1.png"></td>
|
<td width="33%"><img src="https://gogs.io/img/screenshots/1.png"></td>
|
||||||
<td width="33%"><img src="http://gogs.io/img/screenshots/2.png"></td>
|
<td width="33%"><img src="https://gogs.io/img/screenshots/2.png"></td>
|
||||||
<td width="33%"><img src="http://gogs.io/img/screenshots/3.png"></td>
|
<td width="33%"><img src="https://gogs.io/img/screenshots/3.png"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="http://gogs.io/img/screenshots/4.png"></td>
|
<td><img src="https://gogs.io/img/screenshots/4.png"></td>
|
||||||
<td><img src="http://gogs.io/img/screenshots/5.png"></td>
|
<td><img src="https://gogs.io/img/screenshots/5.png"></td>
|
||||||
<td><img src="http://gogs.io/img/screenshots/6.png"></td>
|
<td><img src="https://gogs.io/img/screenshots/6.png"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="http://gogs.io/img/screenshots/7.png"></td>
|
<td><img src="https://gogs.io/img/screenshots/7.png"></td>
|
||||||
<td><img src="http://gogs.io/img/screenshots/8.png"></td>
|
<td><img src="https://gogs.io/img/screenshots/8.png"></td>
|
||||||
<td><img src="http://gogs.io/img/screenshots/9.png"></td>
|
<td><img src="https://gogs.io/img/screenshots/9.png"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -29,20 +29,21 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra
|
||||||
|
|
||||||
- Due to testing purpose, data of [try.gogs.io](https://try.gogs.io) has been reset in **Jan 28, 2015** and will reset multiple times after. Please do **NOT** put your important data on the site.
|
- Due to testing purpose, data of [try.gogs.io](https://try.gogs.io) has been reset in **Jan 28, 2015** and will reset multiple times after. Please do **NOT** put your important data on the site.
|
||||||
- The demo site [try.gogs.io](https://try.gogs.io) is running under `develop` branch.
|
- The demo site [try.gogs.io](https://try.gogs.io) is running under `develop` branch.
|
||||||
- :exclamation::exclamation::exclamation:<span style="color: red">You **MUST** read [CONTRIBUTING.md](CONTRIBUTING.md) before you start filing an issue or making a Pull Request, and **MUST** discuss with us on [Gitter](https://gitter.im/gogits/gogs) for UI changes and feature Pull Requests, otherwise it's high possibilities that we are not going to merge it.</span>:exclamation::exclamation::exclamation:
|
- :bangbang:<span style="color: red">You **MUST** read [CONTRIBUTING.md](CONTRIBUTING.md) before you start filing an issue or making a Pull Request, and **MUST** discuss with us on [Gitter](https://gitter.im/gogits/gogs) for UI changes, otherwise it's high possibilities that we are not going to merge it.</span>:bangbang:
|
||||||
|
- Please [start discussion](http://forum.gogs.io/category/2/general-discussion) or [ask a question](http://forum.gogs.io/category/4/getting-help) on [the forum](http://forum.gogs.io/). GitHub issue tracker only keeps **bugs** and **feature requests**, all other topics will be closed without reason.
|
||||||
- If you think there are vulnerabilities in the project, please talk privately to **u@gogs.io**. Thanks!
|
- If you think there are vulnerabilities in the project, please talk privately to **u@gogs.io**. Thanks!
|
||||||
|
- If you're interested in using APIs, we have experimental support with [documentation](https://github.com/gogits/go-gogs-client/wiki).
|
||||||
|
- If your team/company is using Gogs and would like to put your logo on [our website](http://gogs.io), contact us by any means.
|
||||||
|
|
||||||
#### Other language version
|
[简体中文](README_ZH.md)
|
||||||
|
|
||||||
- [简体中文](README_ZH.md)
|
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
The goal of this project is to make the easiest, fastest, and most painless way of setting up a self-hosted Git service. With Go, this can be done with an independent binary distribution across **ALL platforms** that Go supports, including Linux, Mac OS X, and Windows.
|
The goal of this project is to make the easiest, fastest, and most painless way of setting up a self-hosted Git service. With Go, this can be done with an independent binary distribution across **ALL platforms** that Go supports, including Linux, Mac OS X, Windows and ARM.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
- Please see the [Documentation](http://gogs.io/docs/intro) for project design, known issues, and change log.
|
- Please see the [Documentation](http://gogs.io/docs/intro) for common usages and change log.
|
||||||
- See the [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) to follow the develop team.
|
- See the [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) to follow the develop team.
|
||||||
- Want to try it before doing anything else? Do it [online](https://try.gogs.io/gogs/gogs) or go down to the **Installation -> Install from binary** section!
|
- Want to try it before doing anything else? Do it [online](https://try.gogs.io/gogs/gogs) or go down to the **Installation -> Install from binary** section!
|
||||||
- Having trouble? Get help with [Troubleshooting](http://gogs.io/docs/intro/troubleshooting.html).
|
- Having trouble? Get help with [Troubleshooting](http://gogs.io/docs/intro/troubleshooting.html).
|
||||||
|
@ -63,13 +64,13 @@ The goal of this project is to make the easiest, fastest, and most painless way
|
||||||
- Mail service
|
- Mail service
|
||||||
- Administration panel
|
- Administration panel
|
||||||
- CI integration: [Drone](https://github.com/drone/drone)
|
- CI integration: [Drone](https://github.com/drone/drone)
|
||||||
- Supports MySQL, PostgreSQL, SQLite3 and [TiDB](https://github.com/pingcap/tidb)
|
- Supports MySQL, PostgreSQL, SQLite3 and [TiDB](https://github.com/pingcap/tidb) (experimental)
|
||||||
- Multi-language support ([14 languages](https://crowdin.com/project/gogs))
|
- Multi-language support ([14 languages](https://crowdin.com/project/gogs))
|
||||||
|
|
||||||
## System Requirements
|
## System Requirements
|
||||||
|
|
||||||
- A cheap Raspberry Pi is powerful enough for basic functionality.
|
- A cheap Raspberry Pi is powerful enough for basic functionality.
|
||||||
- At least 2 CPU cores and 1GB RAM would be the baseline for teamwork.
|
- 2 CPU cores and 1GB RAM would be the baseline for teamwork.
|
||||||
|
|
||||||
## Browser Support
|
## Browser Support
|
||||||
|
|
||||||
|
@ -92,6 +93,7 @@ There are 5 ways to install Gogs:
|
||||||
|
|
||||||
- [How To Set Up Gogs on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-gogs-on-ubuntu-14-04)
|
- [How To Set Up Gogs on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-gogs-on-ubuntu-14-04)
|
||||||
- [Run your own GitHub-like service with the help of Docker](http://blog.hypriot.com/post/run-your-own-github-like-service-with-docker/)
|
- [Run your own GitHub-like service with the help of Docker](http://blog.hypriot.com/post/run-your-own-github-like-service-with-docker/)
|
||||||
|
- [使用 Gogs 搭建自己的 Git 服务器](https://mynook.info/blog/post/host-your-own-git-server-using-gogs) (Chinese)
|
||||||
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654) (Chinese)
|
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654) (Chinese)
|
||||||
- [Installing Gogs on FreeBSD](https://www.codejam.info/2015/03/installing-gogs-on-freebsd.html)
|
- [Installing Gogs on FreeBSD](https://www.codejam.info/2015/03/installing-gogs-on-freebsd.html)
|
||||||
- [Gogs on Raspberry Pi](http://blog.meinside.pe.kr/Gogs-on-Raspberry-Pi/)
|
- [Gogs on Raspberry Pi](http://blog.meinside.pe.kr/Gogs-on-Raspberry-Pi/)
|
||||||
|
@ -120,6 +122,7 @@ There are 5 ways to install Gogs:
|
||||||
- System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog).
|
- System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog).
|
||||||
- Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo.
|
- Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo.
|
||||||
- Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan.
|
- Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan.
|
||||||
|
- Thanks [DigitalOcean](https://www.digitalocean.com) for hosting home and demo sites.
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
|
|
10
README_ZH.md
10
README_ZH.md
|
@ -1,15 +1,15 @@
|
||||||
Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?branch=master)](https://travis-ci.org/gogits/gogs)
|
Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?branch=master)](https://travis-ci.org/gogits/gogs)
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
Gogs (Go Git Service) 是一款可轻易搭建的自助 Git 服务。
|
Gogs (Go Git Service) 是一款极易搭建的自助 Git 服务。
|
||||||
|
|
||||||
## 开发目的
|
## 开发目的
|
||||||
|
|
||||||
Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发,并且支持 Go 语言支持的 **所有平台**,包括 Linux、Mac OS X 以及 Windows。
|
Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发,并且支持 Go 语言支持的 **所有平台**,包括 Linux、Mac OS X、Windows 以及 ARM 平台。
|
||||||
|
|
||||||
## 项目概览
|
## 项目概览
|
||||||
|
|
||||||
- 有关项目设计、已知问题和变更日志,请通过 [使用手册](http://gogs.io/docs/intro/) 查看。
|
- 有关基本用法和变更日志,请通过 [使用手册](http://gogs.io/docs/intro/) 查看。
|
||||||
- 您可以到 [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。
|
- 您可以到 [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。
|
||||||
- 想要先睹为快?通过 [在线体验](https://try.gogs.io/gogs/gogs) 或查看 **安装部署 -> 二进制安装** 小节。
|
- 想要先睹为快?通过 [在线体验](https://try.gogs.io/gogs/gogs) 或查看 **安装部署 -> 二进制安装** 小节。
|
||||||
- 使用过程中遇到问题?尝试从 [故障排查](http://gogs.io/docs/intro/troubleshooting.html) 页面获取帮助。
|
- 使用过程中遇到问题?尝试从 [故障排查](http://gogs.io/docs/intro/troubleshooting.html) 页面获取帮助。
|
||||||
|
@ -30,7 +30,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
|
||||||
- 支持邮件服务
|
- 支持邮件服务
|
||||||
- 支持后台管理面板
|
- 支持后台管理面板
|
||||||
- 支持 CI 集成:[Drone](https://github.com/drone/drone)
|
- 支持 CI 集成:[Drone](https://github.com/drone/drone)
|
||||||
- 支持 MySQL、PostgreSQL、SQLite3 和 [TiDB](https://github.com/pingcap/tidb) 数据库
|
- 支持 MySQL、PostgreSQL、SQLite3 和 [TiDB](https://github.com/pingcap/tidb)(实验性支持) 数据库
|
||||||
- 支持多语言本地化([14 种语言]([more](https://crowdin.com/project/gogs)))
|
- 支持多语言本地化([14 种语言]([more](https://crowdin.com/project/gogs)))
|
||||||
|
|
||||||
## 系统要求
|
## 系统要求
|
||||||
|
@ -57,6 +57,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
|
||||||
|
|
||||||
### 使用教程
|
### 使用教程
|
||||||
|
|
||||||
|
- [使用 Gogs 搭建自己的 Git 服务器](https://mynook.info/blog/post/host-your-own-git-server-using-gogs)
|
||||||
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654)
|
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654)
|
||||||
|
|
||||||
### 云端部署
|
### 云端部署
|
||||||
|
@ -79,6 +80,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
|
||||||
- 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。
|
- 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。
|
||||||
- 感谢 [lavachen](http://www.lavachen.cn/) 和 [Rocker](http://weibo.com/rocker1989) 设计的 Logo。
|
- 感谢 [lavachen](http://www.lavachen.cn/) 和 [Rocker](http://weibo.com/rocker1989) 设计的 Logo。
|
||||||
- 感谢 [Crowdin](https://crowdin.com/project/gogs) 提供免费的开源项目本地化支持。
|
- 感谢 [Crowdin](https://crowdin.com/project/gogs) 提供免费的开源项目本地化支持。
|
||||||
|
- 感谢 [DigitalOcean](https://www.digitalocean.com) 提供主站和体验站点的服务器赞助。
|
||||||
|
|
||||||
## 贡献成员
|
## 贡献成员
|
||||||
|
|
||||||
|
|
12
cmd/cert.go
12
cmd/cert.go
|
@ -32,12 +32,12 @@ var CmdCert = cli.Command{
|
||||||
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
|
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
|
||||||
Action: runCert,
|
Action: runCert,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{"host", "", "Comma-separated hostnames and IPs to generate a certificate for", ""},
|
stringFlag("host", "", "Comma-separated hostnames and IPs to generate a certificate for"),
|
||||||
cli.StringFlag{"ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", ""},
|
stringFlag("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521"),
|
||||||
cli.IntFlag{"rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set", ""},
|
intFlag("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set"),
|
||||||
cli.StringFlag{"start-date", "", "Creation date formatted as Jan 1 15:04:05 2011", ""},
|
stringFlag("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011"),
|
||||||
cli.DurationFlag{"duration", 365 * 24 * time.Hour, "Duration that certificate is valid for", ""},
|
durationFlag("duration", 365*24*time.Hour, "Duration that certificate is valid for"),
|
||||||
cli.BoolFlag{"ca", "whether this cert should be its own Certificate Authority", ""},
|
boolFlag("ca", "whether this cert should be its own Certificate Authority"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
// 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func stringFlag(name, value, usage string) cli.StringFlag {
|
||||||
|
return cli.StringFlag{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Usage: usage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolFlag(name, usage string) cli.BoolFlag {
|
||||||
|
return cli.BoolFlag{
|
||||||
|
Name: name,
|
||||||
|
Usage: usage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func intFlag(name string, value int, usage string) cli.IntFlag {
|
||||||
|
return cli.IntFlag{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Usage: usage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func durationFlag(name string, value time.Duration, usage string) cli.DurationFlag {
|
||||||
|
return cli.DurationFlag{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Usage: usage,
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,8 +25,8 @@ var CmdDump = cli.Command{
|
||||||
It can be used for backup and capture Gogs server image to send to maintainer`,
|
It can be used for backup and capture Gogs server image to send to maintainer`,
|
||||||
Action: runDump,
|
Action: runDump,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""},
|
stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
|
||||||
cli.BoolFlag{"verbose, v", "show process details", ""},
|
boolFlag("verbose, v", "show process details"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
94
cmd/serve.go
94
cmd/serve.go
|
@ -33,7 +33,7 @@ var CmdServ = cli.Command{
|
||||||
Description: `Serv provide access auth for repositories`,
|
Description: `Serv provide access auth for repositories`,
|
||||||
Action: runServ,
|
Action: runServ,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""},
|
stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,51 @@ var (
|
||||||
|
|
||||||
func fail(userMessage, logMessage string, args ...interface{}) {
|
func fail(userMessage, logMessage string, args ...interface{}) {
|
||||||
fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
|
fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
|
||||||
|
|
||||||
|
if len(logMessage) > 0 {
|
||||||
log.GitLogger.Fatal(3, logMessage, args...)
|
log.GitLogger.Fatal(3, logMessage, args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.GitLogger.Close()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpdateTask(uuid string, user *models.User, repoUserName, repoName string) {
|
||||||
|
task, err := models.GetUpdateTaskByUUID(uuid)
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrUpdateTaskNotExist(err) {
|
||||||
|
log.GitLogger.Trace("No update task is presented: %s", uuid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.GitLogger.Fatal(2, "GetUpdateTaskByUUID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = models.Update(task.RefName, task.OldCommitID, task.NewCommitID,
|
||||||
|
user.Name, repoUserName, repoName, user.Id); err != nil {
|
||||||
|
log.GitLogger.Error(2, "Update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = models.DeleteUpdateTaskByUUID(uuid); err != nil {
|
||||||
|
log.GitLogger.Fatal(2, "DeleteUpdateTaskByUUID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for running deliver hook and test pull request tasks.
|
||||||
|
reqURL := setting.AppUrl + repoUserName + "/" + repoName + "/tasks/trigger?branch=" +
|
||||||
|
strings.TrimPrefix(task.RefName, "refs/heads/")
|
||||||
|
log.GitLogger.Trace("Trigger task: %s", reqURL)
|
||||||
|
|
||||||
|
resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}).Response()
|
||||||
|
if err == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
log.GitLogger.Error(2, "Fail to trigger task: not 2xx response code")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.GitLogger.Error(2, "Fail to trigger task: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServ(c *cli.Context) {
|
func runServ(c *cli.Context) {
|
||||||
|
@ -95,13 +139,13 @@ func runServ(c *cli.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
verb, args := parseCmd(cmd)
|
verb, args := parseCmd(cmd)
|
||||||
repoPath := strings.Trim(args, "'")
|
repoPath := strings.ToLower(strings.Trim(args, "'"))
|
||||||
rr := strings.SplitN(repoPath, "/", 2)
|
rr := strings.SplitN(repoPath, "/", 2)
|
||||||
if len(rr) != 2 {
|
if len(rr) != 2 {
|
||||||
fail("Invalid repository path", "Invalid repository path: %v", args)
|
fail("Invalid repository path", "Invalid repository path: %v", args)
|
||||||
}
|
}
|
||||||
repoUserName := rr[0]
|
repoUserName := strings.ToLower(rr[0])
|
||||||
repoName := strings.TrimSuffix(rr[1], ".git")
|
repoName := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
|
||||||
|
|
||||||
repoUser, err := models.GetUserByName(repoUserName)
|
repoUser, err := models.GetUserByName(repoUserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -124,6 +168,11 @@ func runServ(c *cli.Context) {
|
||||||
fail("Unknown git command", "Unknown git command %s", verb)
|
fail("Unknown git command", "Unknown git command %s", verb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prohibit push to mirror repositories.
|
||||||
|
if requestedMode > models.ACCESS_MODE_READ && repo.IsMirror {
|
||||||
|
fail("mirror repository is read-only", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Allow anonymous clone for public repositories.
|
// Allow anonymous clone for public repositories.
|
||||||
var (
|
var (
|
||||||
keyID int64
|
keyID int64
|
||||||
|
@ -132,12 +181,12 @@ func runServ(c *cli.Context) {
|
||||||
if requestedMode == models.ACCESS_MODE_WRITE || repo.IsPrivate {
|
if requestedMode == models.ACCESS_MODE_WRITE || repo.IsPrivate {
|
||||||
keys := strings.Split(c.Args()[0], "-")
|
keys := strings.Split(c.Args()[0], "-")
|
||||||
if len(keys) != 2 {
|
if len(keys) != 2 {
|
||||||
fail("Key ID format error", "Invalid key ID: %s", c.Args()[0])
|
fail("Key ID format error", "Invalid key argument: %s", c.Args()[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := models.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64())
|
key, err := models.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail("Key ID format error", "Invalid key ID[%s]: %v", c.Args()[0], err)
|
fail("Invalid key ID", "Invalid key ID[%s]: %v", c.Args()[0], err)
|
||||||
}
|
}
|
||||||
keyID = key.ID
|
keyID = key.ID
|
||||||
|
|
||||||
|
@ -162,7 +211,7 @@ func runServ(c *cli.Context) {
|
||||||
fail("Internal error", "UpdateDeployKey: %v", err)
|
fail("Internal error", "UpdateDeployKey: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user, err = models.GetUserByKeyId(key.ID)
|
user, err = models.GetUserByKeyID(key.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
|
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
|
||||||
}
|
}
|
||||||
|
@ -201,36 +250,7 @@ func runServ(c *cli.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if requestedMode == models.ACCESS_MODE_WRITE {
|
if requestedMode == models.ACCESS_MODE_WRITE {
|
||||||
task, err := models.GetUpdateTaskByUUID(uuid)
|
handleUpdateTask(uuid, user, repoUserName, repoName)
|
||||||
if err != nil {
|
|
||||||
log.GitLogger.Fatal(2, "GetUpdateTaskByUUID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = models.Update(task.RefName, task.OldCommitID, task.NewCommitID,
|
|
||||||
user.Name, repoUserName, repoName, user.Id); err != nil {
|
|
||||||
log.GitLogger.Error(2, "Update: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = models.DeleteUpdateTaskByUUID(uuid); err != nil {
|
|
||||||
log.GitLogger.Fatal(2, "DeleteUpdateTaskByUUID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for running deliver hook and test pull request tasks.
|
|
||||||
reqURL := setting.AppUrl + repoUserName + "/" + repoName + "/tasks/trigger?branch=" +
|
|
||||||
strings.TrimPrefix(task.RefName, "refs/heads/")
|
|
||||||
log.GitLogger.Trace("Trigger task: %s", reqURL)
|
|
||||||
|
|
||||||
resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}).Response()
|
|
||||||
if err == nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode/100 != 2 {
|
|
||||||
log.GitLogger.Error(2, "Fail to trigger task: not 2xx response code")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.GitLogger.Error(2, "Fail to trigger task: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user key activity.
|
// Update user key activity.
|
||||||
|
|
|
@ -20,7 +20,7 @@ var CmdUpdate = cli.Command{
|
||||||
Description: `Update get pushed info and insert into database`,
|
Description: `Update get pushed info and insert into database`,
|
||||||
Action: runUpdate,
|
Action: runUpdate,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""},
|
stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +42,8 @@ func runUpdate(c *cli.Context) {
|
||||||
log.GitLogger.Fatal(2, "refName is empty, shouldn't use")
|
log.GitLogger.Fatal(2, "refName is empty, shouldn't use")
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid := os.Getenv("uuid")
|
|
||||||
|
|
||||||
task := models.UpdateTask{
|
task := models.UpdateTask{
|
||||||
UUID: uuid,
|
UUID: os.Getenv("uuid"),
|
||||||
RefName: args[0],
|
RefName: args[0],
|
||||||
OldCommitID: args[1],
|
OldCommitID: args[1],
|
||||||
NewCommitID: args[2],
|
NewCommitID: args[2],
|
||||||
|
|
49
cmd/web.go
49
cmd/web.go
|
@ -7,7 +7,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
gotmpl "html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/fcgi"
|
"net/http/fcgi"
|
||||||
|
@ -35,11 +35,11 @@ import (
|
||||||
"github.com/gogits/gogs/modules/auth"
|
"github.com/gogits/gogs/modules/auth"
|
||||||
"github.com/gogits/gogs/modules/auth/apiv1"
|
"github.com/gogits/gogs/modules/auth/apiv1"
|
||||||
"github.com/gogits/gogs/modules/avatar"
|
"github.com/gogits/gogs/modules/avatar"
|
||||||
"github.com/gogits/gogs/modules/base"
|
|
||||||
"github.com/gogits/gogs/modules/bindata"
|
"github.com/gogits/gogs/modules/bindata"
|
||||||
"github.com/gogits/gogs/modules/log"
|
"github.com/gogits/gogs/modules/log"
|
||||||
"github.com/gogits/gogs/modules/middleware"
|
"github.com/gogits/gogs/modules/middleware"
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
|
"github.com/gogits/gogs/modules/template"
|
||||||
"github.com/gogits/gogs/routers"
|
"github.com/gogits/gogs/routers"
|
||||||
"github.com/gogits/gogs/routers/admin"
|
"github.com/gogits/gogs/routers/admin"
|
||||||
"github.com/gogits/gogs/routers/api/v1"
|
"github.com/gogits/gogs/routers/api/v1"
|
||||||
|
@ -56,8 +56,8 @@ var CmdWeb = cli.Command{
|
||||||
and it takes care of all the other things for you`,
|
and it takes care of all the other things for you`,
|
||||||
Action: runWeb,
|
Action: runWeb,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{"port, p", "3000", "Temporary port number to prevent conflict", ""},
|
stringFlag("port, p", "3000", "Temporary port number to prevent conflict"),
|
||||||
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""},
|
stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,13 +80,14 @@ func checkVersion() {
|
||||||
|
|
||||||
// Check dependency version.
|
// Check dependency version.
|
||||||
checkers := []VerChecker{
|
checkers := []VerChecker{
|
||||||
{"github.com/go-xorm/xorm", func() string { return xorm.Version }, "0.4.3.0806"},
|
{"github.com/go-xorm/xorm", func() string { return xorm.Version }, "0.4.4.1029"},
|
||||||
{"github.com/Unknwon/macaron", macaron.Version, "0.5.4"},
|
{"github.com/Unknwon/macaron", macaron.Version, "0.5.4"},
|
||||||
{"github.com/macaron-contrib/binding", binding.Version, "0.1.0"},
|
{"github.com/go-macaron/binding", binding.Version, "0.1.0"},
|
||||||
{"github.com/macaron-contrib/cache", cache.Version, "0.1.2"},
|
{"github.com/go-macaron/cache", cache.Version, "0.1.2"},
|
||||||
{"github.com/macaron-contrib/csrf", csrf.Version, "0.0.3"},
|
{"github.com/go-macaron/csrf", csrf.Version, "0.0.3"},
|
||||||
{"github.com/macaron-contrib/i18n", i18n.Version, "0.0.7"},
|
{"github.com/go-macaron/i18n", i18n.Version, "0.0.7"},
|
||||||
{"github.com/macaron-contrib/session", session.Version, "0.1.6"},
|
{"github.com/go-macaron/session", session.Version, "0.1.6"},
|
||||||
|
{"github.com/go-macaron/toolbox", toolbox.Version, "0.1.0"},
|
||||||
{"gopkg.in/ini.v1", ini.Version, "1.3.4"},
|
{"gopkg.in/ini.v1", ini.Version, "1.3.4"},
|
||||||
}
|
}
|
||||||
for _, c := range checkers {
|
for _, c := range checkers {
|
||||||
|
@ -124,7 +125,7 @@ func newMacaron() *macaron.Macaron {
|
||||||
))
|
))
|
||||||
m.Use(macaron.Renderer(macaron.RenderOptions{
|
m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||||
Directory: path.Join(setting.StaticRootPath, "templates"),
|
Directory: path.Join(setting.StaticRootPath, "templates"),
|
||||||
Funcs: []template.FuncMap{base.TemplateFuncs},
|
Funcs: []gotmpl.FuncMap{template.Funcs},
|
||||||
IndentJSON: macaron.Env != macaron.PROD,
|
IndentJSON: macaron.Env != macaron.PROD,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -237,6 +238,13 @@ func runWeb(ctx *cli.Context) {
|
||||||
m.Patch("/hooks/:id:int", bind(api.EditHookOption{}), v1.EditRepoHook)
|
m.Patch("/hooks/:id:int", bind(api.EditHookOption{}), v1.EditRepoHook)
|
||||||
m.Get("/raw/*", middleware.RepoRef(), v1.GetRepoRawFile)
|
m.Get("/raw/*", middleware.RepoRef(), v1.GetRepoRawFile)
|
||||||
m.Get("/archive/*", v1.GetRepoArchive)
|
m.Get("/archive/*", v1.GetRepoArchive)
|
||||||
|
|
||||||
|
m.Group("/keys", func() {
|
||||||
|
m.Combo("").Get(v1.ListRepoDeployKeys).
|
||||||
|
Post(bind(api.CreateDeployKeyOption{}), v1.CreateRepoDeployKey)
|
||||||
|
m.Combo("/:id").Get(v1.GetRepoDeployKey).
|
||||||
|
Delete(v1.DeleteRepoDeploykey)
|
||||||
|
})
|
||||||
}, middleware.ApiRepoAssignment())
|
}, middleware.ApiRepoAssignment())
|
||||||
}, middleware.ApiReqToken())
|
}, middleware.ApiReqToken())
|
||||||
|
|
||||||
|
@ -385,8 +393,8 @@ func runWeb(ctx *cli.Context) {
|
||||||
m.Get("/teams", org.Teams)
|
m.Get("/teams", org.Teams)
|
||||||
m.Get("/teams/:team", org.TeamMembers)
|
m.Get("/teams/:team", org.TeamMembers)
|
||||||
m.Get("/teams/:team/repositories", org.TeamRepositories)
|
m.Get("/teams/:team/repositories", org.TeamRepositories)
|
||||||
m.Get("/teams/:team/action/:action", org.TeamsAction)
|
m.Route("/teams/:team/action/:action", "GET,POST", org.TeamsAction)
|
||||||
m.Get("/teams/:team/action/repo/:action", org.TeamsRepoAction)
|
m.Route("/teams/:team/action/repo/:action", "GET,POST", org.TeamsRepoAction)
|
||||||
}, middleware.OrgAssignment(true, true))
|
}, middleware.OrgAssignment(true, true))
|
||||||
|
|
||||||
m.Group("/:org", func() {
|
m.Group("/:org", func() {
|
||||||
|
@ -462,8 +470,10 @@ func runWeb(ctx *cli.Context) {
|
||||||
m.Post("/delete", repo.DeleteDeployKey)
|
m.Post("/delete", repo.DeleteDeployKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
}, func(ctx *middleware.Context) {
|
||||||
|
ctx.Data["PageIsSettings"] = true
|
||||||
})
|
})
|
||||||
}, reqSignIn, middleware.RepoAssignment(true), reqRepoAdmin)
|
}, reqSignIn, middleware.RepoAssignment(true), reqRepoAdmin, middleware.RepoRef())
|
||||||
|
|
||||||
m.Group("/:username/:reponame", func() {
|
m.Group("/:username/:reponame", func() {
|
||||||
m.Get("/action/:action", repo.Action)
|
m.Get("/action/:action", repo.Action)
|
||||||
|
@ -504,6 +514,7 @@ func runWeb(ctx *cli.Context) {
|
||||||
m.Post("/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost)
|
m.Post("/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost)
|
||||||
m.Get("/edit/:tagname", repo.EditRelease)
|
m.Get("/edit/:tagname", repo.EditRelease)
|
||||||
m.Post("/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)
|
m.Post("/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)
|
||||||
|
m.Post("/delete", repo.DeleteRelease)
|
||||||
}, reqRepoAdmin, middleware.RepoRef())
|
}, reqRepoAdmin, middleware.RepoRef())
|
||||||
|
|
||||||
m.Combo("/compare/*").Get(repo.CompareAndPullRequest).
|
m.Combo("/compare/*").Get(repo.CompareAndPullRequest).
|
||||||
|
@ -511,11 +522,17 @@ func runWeb(ctx *cli.Context) {
|
||||||
}, reqSignIn, middleware.RepoAssignment(true))
|
}, reqSignIn, middleware.RepoAssignment(true))
|
||||||
|
|
||||||
m.Group("/:username/:reponame", func() {
|
m.Group("/:username/:reponame", func() {
|
||||||
m.Get("/releases", middleware.RepoRef(), repo.Releases)
|
m.Group("", func() {
|
||||||
|
m.Get("/releases", repo.Releases)
|
||||||
m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues)
|
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("/labels/", repo.RetrieveLabels, repo.Labels)
|
||||||
m.Get("/milestones", repo.Milestones)
|
m.Get("/milestones", repo.Milestones)
|
||||||
|
}, middleware.RepoRef(),
|
||||||
|
func(ctx *middleware.Context) {
|
||||||
|
ctx.Data["PageIsList"] = true
|
||||||
|
})
|
||||||
|
m.Get("/^:type(issues|pulls)$/:index", repo.ViewIssue)
|
||||||
|
|
||||||
m.Get("/branches", repo.Branches)
|
m.Get("/branches", repo.Branches)
|
||||||
m.Get("/archive/*", repo.Download)
|
m.Get("/archive/*", repo.Download)
|
||||||
|
|
||||||
|
|
14
conf/app.ini
14
conf/app.ini
|
@ -48,6 +48,8 @@ HTTP_ADDR =
|
||||||
HTTP_PORT = 3000
|
HTTP_PORT = 3000
|
||||||
; Disable SSH feature when not available
|
; Disable SSH feature when not available
|
||||||
DISABLE_SSH = false
|
DISABLE_SSH = false
|
||||||
|
; Whether use builtin SSH server or not.
|
||||||
|
START_SSH_SERVER = false
|
||||||
SSH_PORT = 22
|
SSH_PORT = 22
|
||||||
; Disable CDN even in "prod" mode
|
; Disable CDN even in "prod" mode
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
|
@ -116,6 +118,16 @@ DISABLE_MINIMUM_KEY_SIZE_CHECK = false
|
||||||
; Enable captcha validation for registration
|
; Enable captcha validation for registration
|
||||||
ENABLE_CAPTCHA = true
|
ENABLE_CAPTCHA = true
|
||||||
|
|
||||||
|
; used to filter keys which are too short
|
||||||
|
[service.minimum_key_sizes]
|
||||||
|
ED25519 = 256
|
||||||
|
ECDSA = 256
|
||||||
|
NTRU = 1087
|
||||||
|
MCE = 1702
|
||||||
|
McE = 1702
|
||||||
|
RSA = 1024
|
||||||
|
DSA = 1024
|
||||||
|
|
||||||
[webhook]
|
[webhook]
|
||||||
; Hook task queue length
|
; Hook task queue length
|
||||||
QUEUE_LENGTH = 1000
|
QUEUE_LENGTH = 1000
|
||||||
|
@ -322,3 +334,5 @@ it-IT = it
|
||||||
|
|
||||||
[other]
|
[other]
|
||||||
SHOW_FOOTER_BRANDING = false
|
SHOW_FOOTER_BRANDING = false
|
||||||
|
; Show version information about gogs and go in the footer
|
||||||
|
SHOW_FOOTER_VERSION = true
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
# This file lists all PUBLIC individuals having contributed content to the translation.
|
# This file lists all PUBLIC individuals having contributed content to the translation.
|
||||||
# Order of name is meaningless.
|
# Entries are in alphabetical order.
|
||||||
|
|
||||||
Akihiro YAGASAKI <yaggytter@momiage.com>
|
Akihiro YAGASAKI <yaggytter AT momiage DOT com>
|
||||||
Alexander Steinhöfer <kontakt@lx-s.de>
|
Alexander Steinhöfer <kontakt AT lx-s DOT de>
|
||||||
Alexandre Magno <alexandre.mbm@gmail.com>
|
Alexandre Magno <alexandre DOT mbm AT gmail DOT com>
|
||||||
Barış Arda Yılmaz <ardayilmazgamer@gmail.com>
|
Andrey Nering <andrey AT nering DOT com DOT br>
|
||||||
Christoph Kisfeld <christoph.kisfeld@gmail.com>
|
Arthur Aslanyan <arthur DOT e DOT aslanyan AT gmail DOT com>
|
||||||
Daniel Speichert <daniel@speichert.pl>
|
Barış Arda Yılmaz <ardayilmazgamer AT gmail DOT com>
|
||||||
Gregor Santner <gdev@live.de>
|
Christoph Kisfeld <christoph DOT kisfeld AT gmail DOT com>
|
||||||
Huimin Wang <wanghm2009@hotmail.co.jp>
|
Daniel Speichert <daniel AT speichert DOT pl>
|
||||||
ilko <email>
|
Dmitriy Nogay <me AT catwhocode DOT ga>
|
||||||
Thomas Fanninger <gogs.thomas@fanninger.at>
|
Gregor Santner <gdev AT live DOT de>
|
||||||
Łukasz Jan Niemier <lukasz@niemier.pl>
|
Hamid Feizabadi <hamidfzm AT gmail DOT com>
|
||||||
Lafriks <lafriks@gmail.com>
|
Huimin Wang <wanghm2009 AT hotmail DOT co DOT jp>
|
||||||
Luc Stepniewski <luc@stepniewski.fr>
|
ilko
|
||||||
Miguel de la Cruz <miguel@mcrx.me>
|
Lafriks <lafriks AT gmail DOT com>
|
||||||
Marc Schiller <marc@schiller.im>
|
Lauri Ojansivu <x AT xet7 DOT org>
|
||||||
Morten Sørensen <klim8d@gmail.com>
|
Luc Stepniewski <luc AT stepniewski DOT fr>
|
||||||
Natan Albuquerque <natanalbuquerque5@gmail.com>
|
Marc Schiller <marc AT schiller DOT im>
|
||||||
|
Miguel de la Cruz <miguel AT mcrx DOT me>
|
||||||
|
Morten Sørensen <klim8d AT gmail DOT com>
|
||||||
|
Natan Albuquerque <natanalbuquerque5 AT gmail DOT com>
|
||||||
|
Odilon Junior <odilon DOT junior93 AT gmail DOT com>
|
||||||
|
Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>
|
||||||
|
Tilmann Bach <tilmann AT outlook DOT com>
|
||||||
|
Vladimir Vissoultchev <wqweto AT gmail DOT com>
|
||||||
|
YJSoft <yjsoft AT yjsoft DOT pe DOT kr>
|
||||||
|
Łukasz Jan Niemier <lukasz AT niemier DOT pl>
|
|
@ -148,7 +148,6 @@ forgot_password=Забравена парола
|
||||||
forget_password=Забравена парола?
|
forget_password=Забравена парола?
|
||||||
sign_up_now=Нуждаете се от профил? Регистрирайте се сега.
|
sign_up_now=Нуждаете се от профил? Регистрирайте се сега.
|
||||||
confirmation_mail_sent_prompt=Ново писмо за потвърждение е изпратено до <b>%s</b>. Моля проверете пощенската си кутия в рамките на следващите %d часа, за да завършите процеса на регистрация.
|
confirmation_mail_sent_prompt=Ново писмо за потвърждение е изпратено до <b>%s</b>. Моля проверете пощенската си кутия в рамките на следващите %d часа, за да завършите процеса на регистрация.
|
||||||
sign_in_to_account=Влезте с Вашия профил
|
|
||||||
active_your_account=Активиране на профил
|
active_your_account=Активиране на профил
|
||||||
resent_limit_prompt=За съжаление Вие съвсем наскоро изпратихте писмо за активация. Моля изчакайте 3 минути, след което опитайте отново.
|
resent_limit_prompt=За съжаление Вие съвсем наскоро изпратихте писмо за активация. Моля изчакайте 3 минути, след което опитайте отново.
|
||||||
has_unconfirmed_mail=Здравейте %s, имате непотвърден адрес на ел. поща (<b>%s</b>). Ако не сте получили писмо за потвърждение или имате нужда да се изпрати ново писмо, моля щракнете бутона по-долу.
|
has_unconfirmed_mail=Здравейте %s, имате непотвърден адрес на ел. поща (<b>%s</b>). Ако не сте получили писмо за потвърждение или имате нужда да се изпрати ново писмо, моля щракнете бутона по-долу.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Моля активирайте Вашия профил
|
||||||
activate_email=Провери адрес на ел. поща
|
activate_email=Провери адрес на ел. поща
|
||||||
reset_password=Нулиране на паролата
|
reset_password=Нулиране на паролата
|
||||||
register_success=Успешна регистрация и добре дошли
|
register_success=Успешна регистрация и добре дошли
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Да
|
yes=Да
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` трябва да съдържа поне %s знака.`
|
||||||
max_size_error=` трябва да съдържа най-много %s знака.`
|
max_size_error=` трябва да съдържа най-много %s знака.`
|
||||||
email_error=` не е валиден адрес на ел. поща.`
|
email_error=` не е валиден адрес на ел. поща.`
|
||||||
url_error=` не е валиден URL адрес.`
|
url_error=` не е валиден URL адрес.`
|
||||||
|
include_error=` трябва да съдържа текст '%s'.`
|
||||||
unknown_error=Неизвестна грешка:
|
unknown_error=Неизвестна грешка:
|
||||||
captcha_incorrect=Captcha не е потвърдена.
|
captcha_incorrect=Captcha не е потвърдена.
|
||||||
password_not_match=Паролата и потвърждението ѝ не съвпадат.
|
password_not_match=Паролата и потвърждението ѝ не съвпадат.
|
||||||
|
@ -334,8 +335,9 @@ repo_name=Име на хранилището
|
||||||
repo_name_helper=Добро име на хранилище е име, състоящо от кратки, запомнящи се и уникални ключови думи.
|
repo_name_helper=Добро име на хранилище е име, състоящо от кратки, запомнящи се и уникални ключови думи.
|
||||||
visibility=Видимост
|
visibility=Видимост
|
||||||
visiblity_helper=Това хранилище е <span class="ui red text">Частно</span>
|
visiblity_helper=Това хранилище е <span class="ui red text">Частно</span>
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=Административна настройка задължава всички нови хранилища да бъдат <span class="ui red text">Частни</span>
|
||||||
visiblity_fork_helper=(Промяна на тази стойност ще се отрази на всички разклонения)
|
visiblity_fork_helper=(Промяна на тази стойност ще се отрази на всички разклонения)
|
||||||
|
clone_helper=Нуждаете се от помощ при клониране? Посетете <a target="_blank" href="%s">Помощ</a>!
|
||||||
fork_repo=Разклони хранилището
|
fork_repo=Разклони хранилището
|
||||||
fork_from=Разклонение от
|
fork_from=Разклонение от
|
||||||
fork_visiblity_helper=Не може да променяте видимостта на разклонено хранилище.
|
fork_visiblity_helper=Не може да променяте видимостта на разклонено хранилище.
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Инициализиране на това хранилище с из
|
||||||
create_repo=Създай хранилище
|
create_repo=Създай хранилище
|
||||||
default_branch=Клон по подразбиране
|
default_branch=Клон по подразбиране
|
||||||
mirror_interval=Интервал на отразяване (часове)
|
mirror_interval=Интервал на отразяване (часове)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Името на хранилището '%s' е запазено.
|
form.name_reserved=Името на хранилището '%s' е запазено.
|
||||||
form.name_pattern_not_allowed=Име на хранилището от вида '%s' не е позволено.
|
form.name_pattern_not_allowed=Име на хранилището от вида '%s' не е позволено.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=Това хранилище ще бъде <span class="text
|
||||||
migrate_repo=Мигрирай хранилище
|
migrate_repo=Мигрирай хранилище
|
||||||
migrate.clone_address=Адрес за клонирай
|
migrate.clone_address=Адрес за клонирай
|
||||||
migrate.clone_address_desc=Това може да е HTTP/HTTPS/GIT адрес или локален път на сървъра.
|
migrate.clone_address_desc=Това може да е HTTP/HTTPS/GIT адрес или локален път на сървъра.
|
||||||
|
migrate.permission_denied=Недостатъчни права за импорт на локални хранилища.
|
||||||
migrate.invalid_local_path=Невалиден път - не съществува или не е директория.
|
migrate.invalid_local_path=Невалиден път - не съществува или не е директория.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=разклонено от
|
forked_from=разклонено от
|
||||||
fork_from_self=Не можете да разклоните хранилище което си е Ваше!
|
fork_from_self=Не можете да разклоните хранилище което си е Ваше!
|
||||||
copy_link=Копирай
|
copy_link=Копирай
|
||||||
copy_link_success=Копирано!
|
copy_link_success=Копирано!
|
||||||
copy_link_error=Натиснете ⌘-C или Ctrl-C за да копирате
|
copy_link_error=Натиснете ⌘-C или Ctrl-C за да копирате
|
||||||
click_to_copy=Копиране в клипборда
|
|
||||||
copied=Успешно копиране
|
copied=Успешно копиране
|
||||||
clone_helper=Нуждаете се от помощ при клониране? Посетете <a target="_blank" href="%s">Помощ</a>!
|
|
||||||
unwatch=Не следи
|
unwatch=Не следи
|
||||||
watch=Следи
|
watch=Следи
|
||||||
unstar=Не харесвам
|
unstar=Не харесвам
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=Създай ново хранилище чрез ком
|
||||||
push_exist_repo=Предай съществуващо хранилище през командния ред
|
push_exist_repo=Предай съществуващо хранилище през командния ред
|
||||||
repo_is_empty=Това хранилище е празно. Моля проверете по-късно пак!
|
repo_is_empty=Това хранилище е празно. Моля проверете по-късно пак!
|
||||||
|
|
||||||
|
|
||||||
branch=Клон
|
branch=Клон
|
||||||
tree=Дърво
|
tree=Дърво
|
||||||
branch_and_tags=Клонове и маркери
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Клонове
|
branches=Клонове
|
||||||
tags=Маркери
|
tags=Маркери
|
||||||
issues=Проблеми
|
issues=Проблеми
|
||||||
|
@ -455,9 +459,9 @@ issues.num_comments=%d коментара
|
||||||
issues.commented_at=`коментира <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at=`коментира <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content=Все още няма съдържание.
|
issues.no_content=Все още няма съдържание.
|
||||||
issues.close_issue=Затвори
|
issues.close_issue=Затвори
|
||||||
issues.close_comment_issue=Затвори и коментирай
|
issues.close_comment_issue=Kоментирай и затвори
|
||||||
issues.reopen_issue=Отвори повторно
|
issues.reopen_issue=Отвори повторно
|
||||||
issues.reopen_comment_issue=Отвори повторно и коментирай
|
issues.reopen_comment_issue=Kоментирай и oтвори отново
|
||||||
issues.create_comment=Коментирай
|
issues.create_comment=Коментирай
|
||||||
issues.closed_at=`затвори <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
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>`
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=Изтрий етикет
|
||||||
issues.label_deletion_desc=При изтриване на този етикет ще се премахне информацията за него във всички свързани проблеми. Желаете ли да продължите?
|
issues.label_deletion_desc=При изтриване на този етикет ще се премахне информацията за него във всички свързани проблеми. Желаете ли да продължите?
|
||||||
issues.label_deletion_success=Етикетът е изтрит успешно!
|
issues.label_deletion_success=Етикетът е изтрит успешно!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Сравни промените
|
pulls.compare_changes=Сравни промените
|
||||||
pulls.compare_changes_desc=Сравнява двата клона и създава заявка за сливане за разликите помежду им.
|
pulls.compare_changes_desc=Сравнява двата клона и създава заявка за сливане за разликите помежду им.
|
||||||
pulls.compare_base=родителска версия
|
pulls.compare_base=родителска версия
|
||||||
|
@ -499,7 +504,7 @@ pulls.reopen_to_merge=Моля повторно отворете тази зая
|
||||||
pulls.merged=Обединени
|
pulls.merged=Обединени
|
||||||
pulls.has_merged=Тази заявка за сливане е обединена успешно!
|
pulls.has_merged=Тази заявка за сливане е обединена успешно!
|
||||||
pulls.data_broken=Данните от тази заявка за сливане са невалидни поради изтрита информация за някое разклонение.
|
pulls.data_broken=Данните от тази заявка за сливане са невалидни поради изтрита информация за някое разклонение.
|
||||||
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
|
pulls.is_checking=Проверката за конфликт все още е в ход. Моля обновете страницата след малко.
|
||||||
pulls.can_auto_merge_desc=Можете да извършвате авто-обединяване на тази заявка за сливане.
|
pulls.can_auto_merge_desc=Можете да извършвате авто-обединяване на тази заявка за сливане.
|
||||||
pulls.cannot_auto_merge_desc=Не можете да извършите авто-обединяване, защото съществуват конфликти между ревизиите.
|
pulls.cannot_auto_merge_desc=Не можете да извършите авто-обединяване, защото съществуват конфликти между ревизиите.
|
||||||
pulls.cannot_auto_merge_helper=Моля, използвайте инструменти на командния ред за да отстраните проблема.
|
pulls.cannot_auto_merge_helper=Моля, използвайте инструменти на командния ред за да отстраните проблема.
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Потвърди изтриването
|
||||||
settings.add_collaborator=Добави нов сътрудник
|
settings.add_collaborator=Добави нов сътрудник
|
||||||
settings.add_collaborator_success=Добавен е нов сътрудник.
|
settings.add_collaborator_success=Добавен е нов сътрудник.
|
||||||
settings.remove_collaborator_success=Сътрудникът е премахнат.
|
settings.remove_collaborator_success=Сътрудникът е премахнат.
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=Потребителят е член на организацията и не може да бъде добавен като сътрудник.
|
settings.user_is_org_member=Потребителят е член на организацията и не може да бъде добавен като сътрудник.
|
||||||
settings.add_webhook=Добави уеб-кука
|
settings.add_webhook=Добави уеб-кука
|
||||||
settings.hooks_desc=Уеб-куките много приличат на обикновен HTTP POST тригер. Когато нещо се случи в Gogs, ние ще изпратим уведомление до сървъра, който посочите. Научете повече в <a target="_blank" href="%s">Ръководство за уеб-куки</a>.
|
settings.hooks_desc=Уеб-куките много приличат на обикновен HTTP POST тригер. Когато нещо се случи в Gogs, ние ще изпратим уведомление до сървъра, който посочите. Научете повече в <a target="_blank" href="%s">Ръководство за уеб-куки</a>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Стабилни
|
||||||
release.edit=редактиране
|
release.edit=редактиране
|
||||||
release.ahead=<strong>%d</strong> ревизии на %s след това издание
|
release.ahead=<strong>%d</strong> ревизии на %s след това издание
|
||||||
release.source_code=Изходен код
|
release.source_code=Изходен код
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Име на маркера
|
release.tag_name=Име на маркера
|
||||||
release.target=Цел
|
release.target=Цел
|
||||||
release.tag_helper=Изберете съществуващ маркер или създайте нов маркер по време на публикуване.
|
release.tag_helper=Изберете съществуващ маркер или създайте нов маркер по време на публикуване.
|
||||||
release.release_title=Заглавие на изданието
|
release.title=Title
|
||||||
release.content_with_md=Съдържание с <a href="%s">Markdown</a>
|
release.content=Content
|
||||||
release.write=Писане
|
release.write=Писане
|
||||||
release.preview=Преглед
|
release.preview=Преглед
|
||||||
release.content_placeholder=Напишете някакво съдържание
|
|
||||||
release.loading=Зареждане...
|
release.loading=Зареждане...
|
||||||
release.prerelease_desc=Това е предварително издание
|
release.prerelease_desc=Това е предварително издание
|
||||||
release.prerelease_helper=Ние ще отбележим, че това издание не е завършено за употреба.
|
release.prerelease_helper=Ние ще отбележим, че това издание не е завършено за употреба.
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=Публикувай издание
|
release.publish=Публикувай издание
|
||||||
release.save_draft=Запис на чернова
|
release.save_draft=Запис на чернова
|
||||||
release.edit_release=Редактирай издание
|
release.edit_release=Редактирай издание
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Издание с това име на маркер вече съществува.
|
release.tag_name_already_exist=Издание с това име на маркер вече съществува.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Име на организацията
|
org_name_holder=Име на организацията
|
||||||
|
org_full_name_holder=Пълно име на организацията
|
||||||
org_name_helper=Добрите имена на организация са кратки и запомнящи се.
|
org_name_helper=Добрите имена на организация са кратки и запомнящи се.
|
||||||
create_org=Създай организация
|
create_org=Създай организация
|
||||||
repo_updated=Обновено
|
repo_updated=Обновено
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=Изтрий всички неактивн
|
||||||
dashboard.delete_inactivate_accounts_success=Всички неактивни профили са изтрити успешно.
|
dashboard.delete_inactivate_accounts_success=Всички неактивни профили са изтрити успешно.
|
||||||
dashboard.delete_repo_archives=Изтрий всички архиви на хранилища
|
dashboard.delete_repo_archives=Изтрий всички архиви на хранилища
|
||||||
dashboard.delete_repo_archives_success=Всички архиви на хранилищата са изтрити успешно.
|
dashboard.delete_repo_archives_success=Всички архиви на хранилищата са изтрити успешно.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Почисти изтрити данни в хранилищата
|
dashboard.git_gc_repos=Почисти изтрити данни в хранилищата
|
||||||
dashboard.git_gc_repos_success=Всички хранилища са почистени от изтрити данни успешно.
|
dashboard.git_gc_repos_success=Всички хранилища са почистени от изтрити данни успешно.
|
||||||
dashboard.resync_all_sshkeys=Презапис на ".ssh/authorized_keys" файл (внимание: не-Gogs ключове ще бъдат загубени)
|
dashboard.resync_all_sshkeys=Презапис на ".ssh/authorized_keys" файл (внимание: не-Gogs ключове ще бъдат загубени)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Редактирай профил
|
||||||
users.is_activated=Този профил е активиран
|
users.is_activated=Този профил е активиран
|
||||||
users.is_admin=Този профил има административни права
|
users.is_admin=Този профил има административни права
|
||||||
users.allow_git_hook=Този профил има разрешение да създава Git куки
|
users.allow_git_hook=Този профил има разрешение да създава Git куки
|
||||||
|
users.allow_import_local=Този профил има права за импорт на локални хранилища
|
||||||
users.update_profile=Обнови профила
|
users.update_profile=Обнови профила
|
||||||
users.delete_account=Изтрий този профил
|
users.delete_account=Изтрий този профил
|
||||||
users.still_own_repo=Този профил притежава поне едно хранилище. Първо трябва да изтриете хранилището или да го прехвърлите на друг потребител.
|
users.still_own_repo=Този профил притежава поне едно хранилище. Първо трябва да изтриете хранилището или да го прехвърлите на друг потребител.
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=Системното съобщение е изтрито
|
||||||
[action]
|
[action]
|
||||||
create_repo=създаде хранилище <a href="%s"> %s</a>
|
create_repo=създаде хранилище <a href="%s"> %s</a>
|
||||||
rename_repo=преименува хранилище от <code>%[1]s</code> на <a href="%[2]s">%[3]s</a>
|
rename_repo=преименува хранилище от <code>%[1]s</code> на <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=предаде към <a href="%s/src/%s">%[2]s</a> в <a href="%[1]s">%[3]s</a>
|
commit_repo=предаде към <a href="%[1]s/src/%[2]s">%[3]s</a> в <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`отвори проблем <a href="%s/issues/%s">%s#%[2]s"</a>`
|
create_issue=`отвори проблем <a href="%s/issues/%s">%s#%[2]s"</a>`
|
||||||
create_pull_request=`създаде заявка за сливане <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`създаде заявка за сливане <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`коментира проблем <a href="%s/issues/%s">%s#%[2]s"</a>`
|
comment_issue=`коментира проблем <a href="%s/issues/%s">%s#%[2]s"</a>`
|
||||||
|
@ -974,13 +991,13 @@ now=сега
|
||||||
1w=%s 1 седмица
|
1w=%s 1 седмица
|
||||||
1mon=%s 1 месец
|
1mon=%s 1 месец
|
||||||
1y=%s 1 година
|
1y=%s 1 година
|
||||||
seconds=%s %d секунди
|
seconds=%[2]s %[1]d секунди
|
||||||
minutes=%s %d минути
|
minutes=%[2]s %[1]d минути
|
||||||
hours=%s %d часа
|
hours=%[2]s %[1]d часа
|
||||||
days=%s %d дни
|
days=%[2]s %[1]d дни
|
||||||
weeks=%s %d седмици
|
weeks=%[2]s %[1]d седмици
|
||||||
months=%s %d месеца
|
months=%[2]s %[1]d месеца
|
||||||
years=%s %d години
|
years=%[2]s %[1]d години
|
||||||
raw_seconds=секунди
|
raw_seconds=секунди
|
||||||
raw_minutes=минути
|
raw_minutes=минути
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ admin_title=Konto-Einstellungen für den Administrator
|
||||||
admin_name=Benutzername
|
admin_name=Benutzername
|
||||||
admin_password=Passwort
|
admin_password=Passwort
|
||||||
confirm_password=Passwort bestätigen
|
confirm_password=Passwort bestätigen
|
||||||
admin_email=E-Mail
|
admin_email=Administrator E-Mail
|
||||||
install_gogs=Gogs installieren
|
install_gogs=Gogs installieren
|
||||||
test_git_failed=Fehler beim Test des 'git' Kommandos: %v
|
test_git_failed=Fehler beim Test des 'git' Kommandos: %v
|
||||||
sqlite3_not_available=Ihre Gogs-Version unterstützt kein SQLite3, bitte lade dir die offizielle binäre Version von %s herunter, NICHT die gobuild-Version.
|
sqlite3_not_available=Ihre Gogs-Version unterstützt kein SQLite3, bitte lade dir die offizielle binäre Version von %s herunter, NICHT die gobuild-Version.
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Passwort vergessen
|
||||||
forget_password=Passwort vergessen?
|
forget_password=Passwort vergessen?
|
||||||
sign_up_now=Du willst ein Konto? Jetzt registrieren!
|
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.
|
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_to_account=Mit deinem Konto anmelden
|
|
||||||
active_your_account=Aktiviere dein Konto
|
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.
|
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ötigst, klicke bitte auf den folgenden Button.
|
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ötigst, klicke bitte auf den folgenden Button.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Bitte aktiviere dein Konto
|
||||||
activate_email=Verifiziere deine E-Mail-Adresse
|
activate_email=Verifiziere deine E-Mail-Adresse
|
||||||
reset_password=Setze dein Passwort zurück
|
reset_password=Setze dein Passwort zurück
|
||||||
register_success=Registrierung erfolgreich, Willkommen
|
register_success=Registrierung erfolgreich, Willkommen
|
||||||
|
register_notify=Willkommen an Bord
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Ja
|
yes=Ja
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` muss mindestens %s Zeichen enthalten.`
|
||||||
max_size_error=` darf höchstens %s Zeichen enthalten.`
|
max_size_error=` darf höchstens %s Zeichen enthalten.`
|
||||||
email_error=` ist keine gültige E-Mail-Adresse.`
|
email_error=` ist keine gültige E-Mail-Adresse.`
|
||||||
url_error=` ist keine gültige URL.`
|
url_error=` ist keine gültige URL.`
|
||||||
|
include_error=`muss den Substring ‚%s‘ enthalten.`
|
||||||
unknown_error=Unbekannter Fehler:
|
unknown_error=Unbekannter Fehler:
|
||||||
captcha_incorrect=Captcha stimmt nicht überein.
|
captcha_incorrect=Captcha stimmt nicht überein.
|
||||||
password_not_match=Die Passwörter stimmen nicht überein.
|
password_not_match=Die Passwörter stimmen nicht überein.
|
||||||
|
@ -336,6 +337,7 @@ visibility=Sichtbarkeit
|
||||||
visiblity_helper=Diese Repository ist <span class="ui red text">Privat</span>
|
visiblity_helper=Diese Repository ist <span class="ui red text">Privat</span>
|
||||||
visiblity_helper_forced=Der Administrator hat festgelegt, dass alle neuen Repositories <span class="ui red text">privat</span> sein müssen
|
visiblity_helper_forced=Der Administrator hat festgelegt, dass alle neuen Repositories <span class="ui red text">privat</span> sein müssen
|
||||||
visiblity_fork_helper=(Eine Änderung dieses Wertes wirkt sich auf alle Forks aus)
|
visiblity_fork_helper=(Eine Änderung dieses Wertes wirkt sich auf alle Forks aus)
|
||||||
|
clone_helper=Du brauchst Hilfe beim Klonen? Hier gibt es <a target="_blank" href="%s">Hilfe</a>!
|
||||||
fork_repo=Repository abspalten
|
fork_repo=Repository abspalten
|
||||||
fork_from=Forken von
|
fork_from=Forken von
|
||||||
fork_visiblity_helper=Die Sichtbarkeit von geforkten Repositories ist nicht veränderbar.
|
fork_visiblity_helper=Die Sichtbarkeit von geforkten Repositories ist nicht veränderbar.
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Repository mit ausgewählten Dateien und Vorlagen initialisieren
|
||||||
create_repo=Repository erstellen
|
create_repo=Repository erstellen
|
||||||
default_branch=Standard-Branch
|
default_branch=Standard-Branch
|
||||||
mirror_interval=Spiegel-Intervall (in Stunden)
|
mirror_interval=Spiegel-Intervall (in Stunden)
|
||||||
|
watchers=Beobachter
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Repository-Name '%s' ist bereits vergeben.
|
form.name_reserved=Repository-Name '%s' ist bereits vergeben.
|
||||||
form.name_pattern_not_allowed=Repository-Namesmuster '%s' ist nicht zulässig.
|
form.name_pattern_not_allowed=Repository-Namesmuster '%s' ist nicht zulässig.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=Diese Repository wird ein <span class="text blue">Spiegel</s
|
||||||
migrate_repo=Repository migrieren
|
migrate_repo=Repository migrieren
|
||||||
migrate.clone_address=Adresse kopieren
|
migrate.clone_address=Adresse kopieren
|
||||||
migrate.clone_address_desc=Dies kann eine HTTP/HTTPS/GIT URL oder ein lokaler Serverpfad sein.
|
migrate.clone_address_desc=Dies kann eine HTTP/HTTPS/GIT URL oder ein lokaler Serverpfad sein.
|
||||||
|
migrate.permission_denied=Ihnen fehlen die Rechte zum Importieren lokaler Repositorys.
|
||||||
migrate.invalid_local_path=Lokaler Pfad ist ungültig, er existiert nicht oder ist kein Ordner.
|
migrate.invalid_local_path=Lokaler Pfad ist ungültig, er existiert nicht oder ist kein Ordner.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=Geforkt von
|
forked_from=Geforkt von
|
||||||
fork_from_self=Sie können keine Repository forken, welche ihnen gehört!
|
fork_from_self=Sie können keine Repository forken, welche ihnen gehört!
|
||||||
copy_link=Kopieren
|
copy_link=Kopieren
|
||||||
copy_link_success=Kopiert!
|
copy_link_success=Kopiert!
|
||||||
copy_link_error=Drücke ⌘-C oder Strg-C zum Kopieren
|
copy_link_error=Drücke ⌘-C oder Strg-C zum Kopieren
|
||||||
click_to_copy=In Zwischenablage kopieren
|
|
||||||
copied=Kopiert OK
|
copied=Kopiert OK
|
||||||
clone_helper=Du brauchst Hilfe beim Klonen? Hier gibt es <a target="_blank" href="%s">Hilfe</a>!
|
|
||||||
unwatch=Nicht mehr beobachten
|
unwatch=Nicht mehr beobachten
|
||||||
watch=Beobachten
|
watch=Beobachten
|
||||||
unstar=Markierung aufheben
|
unstar=Markierung aufheben
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=Erstelle eine neue Repository mittels Kommandozeile
|
||||||
push_exist_repo=Übertrage eine existierende Repository von der Kommandozeile
|
push_exist_repo=Übertrage eine existierende Repository von der Kommandozeile
|
||||||
repo_is_empty=Das Repository ist leer, bitte komm später wieder!
|
repo_is_empty=Das Repository ist leer, bitte komm später wieder!
|
||||||
|
|
||||||
|
|
||||||
branch=Branch
|
branch=Branch
|
||||||
tree=Struktur
|
tree=Struktur
|
||||||
branch_and_tags=Branches & Tags
|
filter_branch_and_tag=Nach Zweig oder Tag filtern
|
||||||
branches=Branches
|
branches=Branches
|
||||||
tags=Tags
|
tags=Tags
|
||||||
issues=Issues
|
issues=Issues
|
||||||
|
@ -455,9 +459,9 @@ issues.num_comments=%d Kommentare
|
||||||
issues.commented_at=`kommentiert in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at=`kommentiert in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content=Hier gibt es bis jetzt noch keinen Inhalt.
|
issues.no_content=Hier gibt es bis jetzt noch keinen Inhalt.
|
||||||
issues.close_issue=Schließen
|
issues.close_issue=Schließen
|
||||||
issues.close_comment_issue=Schließen und kommentieren
|
issues.close_comment_issue=Kommentieren und schließen
|
||||||
issues.reopen_issue=Wiedereröffnen
|
issues.reopen_issue=Wiedereröffnen
|
||||||
issues.reopen_comment_issue=Wiedereröffnen und kommentieren
|
issues.reopen_comment_issue=Kommentieren und wiedereröffnen
|
||||||
issues.create_comment=Kommentieren
|
issues.create_comment=Kommentieren
|
||||||
issues.closed_at=`geschlossen in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at=`geschlossen in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at=`wiedereröffnet in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at=`wiedereröffnet in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=Label Löschung
|
||||||
issues.label_deletion_desc=Das Löschen eines Labels entfernt es von allen verknüpften Issues. Möchtest du fortfahren?
|
issues.label_deletion_desc=Das Löschen eines Labels entfernt es von allen verknüpften Issues. Möchtest du fortfahren?
|
||||||
issues.label_deletion_success=Label wurde erfolgreich gelöscht!
|
issues.label_deletion_success=Label wurde erfolgreich gelöscht!
|
||||||
|
|
||||||
|
pulls.new=Neuer Pull-Request
|
||||||
pulls.compare_changes=Änderungen vergleichen
|
pulls.compare_changes=Änderungen vergleichen
|
||||||
pulls.compare_changes_desc=Vergleiche zwei Branches und erstelle einen Pull Request für die Änderungen.
|
pulls.compare_changes_desc=Vergleiche zwei Branches und erstelle einen Pull Request für die Änderungen.
|
||||||
pulls.compare_base=Base
|
pulls.compare_base=Base
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=Titel
|
||||||
milestones.desc=Beschreibung
|
milestones.desc=Beschreibung
|
||||||
milestones.due_date=Fälligkeitsdatum (optional)
|
milestones.due_date=Fälligkeitsdatum (optional)
|
||||||
milestones.clear=Bereinigen
|
milestones.clear=Bereinigen
|
||||||
milestones.invalid_due_date_format=Format des Fälligkeitsdatums ist ungültig. Es muss das Format 'Jahr-mm-dd' haben.
|
milestones.invalid_due_date_format=Format des Fälligkeitsdatums ist ungültig. Es muss das Format 'JJJJ-mm-dd' haben.
|
||||||
milestones.create_success=Meilenstein '%s' wurde erfolgreich erstellt!
|
milestones.create_success=Meilenstein '%s' wurde erfolgreich erstellt!
|
||||||
milestones.edit=Meilenstein bearbeiten
|
milestones.edit=Meilenstein bearbeiten
|
||||||
milestones.edit_subheader=Benutze eine bessere Beschreibung für die Meilensteine, um die Menschen nicht zu verwirren.
|
milestones.edit_subheader=Benutze eine bessere Beschreibung für die Meilensteine, um die Menschen nicht zu verwirren.
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Löschen
|
||||||
settings.add_collaborator=Mitarbeiter hinzufügen
|
settings.add_collaborator=Mitarbeiter hinzufügen
|
||||||
settings.add_collaborator_success=Mitarbeiter hinzugefügt
|
settings.add_collaborator_success=Mitarbeiter hinzugefügt
|
||||||
settings.remove_collaborator_success=Mitarbeiter entfernt
|
settings.remove_collaborator_success=Mitarbeiter entfernt
|
||||||
|
settings.search_user_placeholder=Benutzer suchen...
|
||||||
settings.user_is_org_member=Benutzer ist ein Organisationsmitglied und kann nicht als Mitarbeiter hinzugefügt werden.
|
settings.user_is_org_member=Benutzer ist ein Organisationsmitglied und kann nicht als Mitarbeiter hinzugefügt werden.
|
||||||
settings.add_webhook=Webhook hinzufügen
|
settings.add_webhook=Webhook hinzufügen
|
||||||
settings.hooks_desc=Webhooks erlauben es dir, externe Dienste zu informieren, wenn etwas bestimmtes in deiner Repository passiert. Gogs sendet dann einen POST-Request an alle angegebenen URLs. Erfahre mehr in unserer <a target="_blank" href="%s">Webhooks Guide</a>.
|
settings.hooks_desc=Webhooks erlauben es dir, externe Dienste zu informieren, wenn etwas bestimmtes in deiner Repository passiert. Gogs sendet dann einen POST-Request an alle angegebenen URLs. Erfahre mehr in unserer <a target="_blank" href="%s">Webhooks Guide</a>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Endversion
|
||||||
release.edit=bearbeiten
|
release.edit=bearbeiten
|
||||||
release.ahead=<strong>%d</strong> Commits zu %s seit diesem Release
|
release.ahead=<strong>%d</strong> Commits zu %s seit diesem Release
|
||||||
release.source_code=Quelltext
|
release.source_code=Quelltext
|
||||||
|
release.new_subheader=Versionen des Projekts veröffentlichen.
|
||||||
|
release.edit_subheader=Detaillierte Änderungsprotokolle können Benutzern helfen zu verstehen, was verbessert wurde.
|
||||||
release.tag_name=Tag-Name
|
release.tag_name=Tag-Name
|
||||||
release.target=Ziel
|
release.target=Ziel
|
||||||
release.tag_helper=Wähle ein neues Tag oder erstelle ein Tag beim Veröffentlichen.
|
release.tag_helper=Wähle ein neues Tag oder erstelle ein Tag beim Veröffentlichen.
|
||||||
release.release_title=Release-Titel
|
release.title=Titel
|
||||||
release.content_with_md=Inhalt mit <a href="%s">Markdown</a>
|
release.content=Inhalt
|
||||||
release.write=Schreiben
|
release.write=Schreiben
|
||||||
release.preview=Vorschau
|
release.preview=Vorschau
|
||||||
release.content_placeholder=Schreibe hier etwas
|
|
||||||
release.loading=Laden…
|
release.loading=Laden…
|
||||||
release.prerelease_desc=Dies ist eine Pre-Release-Version
|
release.prerelease_desc=Dies ist eine Pre-Release-Version
|
||||||
release.prerelease_helper=Wir möchten darauf hinweisen, dass dieses Release nicht für den produktiven Einsatz gedacht ist.
|
release.prerelease_helper=Wir möchten darauf hinweisen, dass dieses Release nicht für den produktiven Einsatz gedacht ist.
|
||||||
|
release.cancel=Abbrechen
|
||||||
release.publish=Release veröffentlichen
|
release.publish=Release veröffentlichen
|
||||||
release.save_draft=Entwurf speichern
|
release.save_draft=Entwurf speichern
|
||||||
release.edit_release=Release bearbeiten
|
release.edit_release=Release bearbeiten
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Ein Release mit diesem Tag existiert bereits.
|
release.tag_name_already_exist=Ein Release mit diesem Tag existiert bereits.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Name der Organisation
|
org_name_holder=Name der Organisation
|
||||||
|
org_full_name_holder=Vollständiger Name der Organisation
|
||||||
org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam.
|
org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam.
|
||||||
create_org=Organisation erstellen
|
create_org=Organisation erstellen
|
||||||
repo_updated=Aktualisiert
|
repo_updated=Aktualisiert
|
||||||
|
@ -685,7 +699,7 @@ settings.delete_prompt=Die Organisation wird dauerhaft gelöscht. Dies kann <str
|
||||||
settings.confirm_delete_account=Löschen
|
settings.confirm_delete_account=Löschen
|
||||||
settings.delete_org_title=Organisation löschen
|
settings.delete_org_title=Organisation löschen
|
||||||
settings.delete_org_desc=Diese Organisation wird dauerhaft gelöscht, möchtest du fortfahren?
|
settings.delete_org_desc=Diese Organisation wird dauerhaft gelöscht, möchtest du fortfahren?
|
||||||
settings.hooks_desc=Füge Webhooks hinzu, die für <strong>alle</strong> Repositorys dieser Organisation ausgelöst werden.
|
settings.hooks_desc=Füge Webhooks hinzu, die für <strong>alle</strong> Repositories dieser Organisation ausgelöst werden.
|
||||||
|
|
||||||
members.public=Öffentlich
|
members.public=Öffentlich
|
||||||
members.public_helper=Privat machen
|
members.public_helper=Privat machen
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=inaktive Konten löschen
|
||||||
dashboard.delete_inactivate_accounts_success=Alle inaktiven Konten wurden erfolgreich gelöscht.
|
dashboard.delete_inactivate_accounts_success=Alle inaktiven Konten wurden erfolgreich gelöscht.
|
||||||
dashboard.delete_repo_archives=Alle Repository-Archive löschen
|
dashboard.delete_repo_archives=Alle Repository-Archive löschen
|
||||||
dashboard.delete_repo_archives_success=Alle Repository-Archive wurden gelöscht.
|
dashboard.delete_repo_archives_success=Alle Repository-Archive wurden gelöscht.
|
||||||
|
dashboard.delete_missing_repos=Löschen Sie alle Repository-Datensätze, mit verlorenen Git-Dateien
|
||||||
|
dashboard.delete_missing_repos_success=Alle Repository-Datensätze, mit verlorenen Git-Dateien wurden erfolgreich gelöscht.
|
||||||
dashboard.git_gc_repos=Führe Garbage Collection auf Repositories aus
|
dashboard.git_gc_repos=Führe Garbage Collection auf Repositories aus
|
||||||
dashboard.git_gc_repos_success=Garbage Collection wurde auf allen Repositories erfolgreich ausgeführt.
|
dashboard.git_gc_repos_success=Garbage Collection wurde auf allen Repositories erfolgreich ausgeführt.
|
||||||
dashboard.resync_all_sshkeys=Überschreibe '.ssh/authorized_keys' Datei (Warnung: Keys, die nicht zu Gogs gehören gehen verloren)
|
dashboard.resync_all_sshkeys=Überschreibe '.ssh/authorized_keys' Datei (Warnung: Keys, die nicht zu Gogs gehören gehen verloren)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Konto bearbeiten
|
||||||
users.is_activated=Dieses Konto ist aktiviert
|
users.is_activated=Dieses Konto ist aktiviert
|
||||||
users.is_admin=Dieses Konto hat Administratorrechte
|
users.is_admin=Dieses Konto hat Administratorrechte
|
||||||
users.allow_git_hook=Dieses Konto ist berechtigt, Git-Hooks zu erstellen
|
users.allow_git_hook=Dieses Konto ist berechtigt, Git-Hooks zu erstellen
|
||||||
|
users.allow_import_local=Dieses Konto ist berechtigt, lokale Repositorys zu importieren
|
||||||
users.update_profile=Kontoprofil aktualisieren
|
users.update_profile=Kontoprofil aktualisieren
|
||||||
users.delete_account=Dieses Konto löschen
|
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_own_repo=Dieses Konto besitzt noch Repositories. Diese müssen zuerst gelöscht oder übertragen werden.
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=System-Mitteilung erfolgreich gelöscht.
|
||||||
[action]
|
[action]
|
||||||
create_repo=hat Repository <a href="%s">%s</a> erstellt
|
create_repo=hat Repository <a href="%s">%s</a> erstellt
|
||||||
rename_repo=das Repository wurde umbenannt von <code>%[1]s</code> zu <a href="%[2]s">%[3]s</a>
|
rename_repo=das Repository wurde umbenannt von <code>%[1]s</code> zu <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=hat nach <a href="%s/src/%s">%[2]s</a> in <a href="%[1]s">%[3]s</a> gepusht
|
commit_repo=hat nach <a href="%[1]s/src/%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a> gepusht
|
||||||
create_issue=`hat Issue <a href="%s/issues/%s">%s#%[2]s</a> eröffnet`
|
create_issue=`hat Issue <a href="%s/issues/%s">%s#%[2]s</a> eröffnet`
|
||||||
create_pull_request=`Pull-Anforderung erstellt <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`Pull-Anforderung erstellt <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`hat Issue <a href="%s/issues/%s">%s#%[2]s</a> kommentiert`
|
comment_issue=`hat Issue <a href="%s/issues/%s">%s#%[2]s</a> kommentiert`
|
||||||
|
|
|
@ -148,7 +148,6 @@ forgot_password= Forgot Password
|
||||||
forget_password = Forgot password?
|
forget_password = Forgot password?
|
||||||
sign_up_now = Need an account? Sign up now.
|
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.
|
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_to_account = Sign in to your account
|
|
||||||
active_your_account = Activate 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.
|
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.
|
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.
|
||||||
|
@ -165,6 +164,7 @@ activate_account = Please activate your account
|
||||||
activate_email = Verify your e-mail address
|
activate_email = Verify your e-mail address
|
||||||
reset_password = Reset your password
|
reset_password = Reset your password
|
||||||
register_success = Register success, Welcome
|
register_success = Register success, Welcome
|
||||||
|
register_notify = Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes = Yes
|
yes = Yes
|
||||||
|
@ -337,6 +337,7 @@ visibility = Visibility
|
||||||
visiblity_helper = This repository is <span class="ui red text">Private</span>
|
visiblity_helper = This repository is <span class="ui red text">Private</span>
|
||||||
visiblity_helper_forced = Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced = Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
||||||
visiblity_fork_helper = (Change of this value will affect all forks)
|
visiblity_fork_helper = (Change of this value will affect all forks)
|
||||||
|
clone_helper = Need help cloning? Visit <a target="_blank" href="%s">Help</a>!
|
||||||
fork_repo = Fork Repository
|
fork_repo = Fork Repository
|
||||||
fork_from = Fork From
|
fork_from = Fork From
|
||||||
fork_visiblity_helper = You cannot alter the visibility of a forked repository.
|
fork_visiblity_helper = You cannot alter the visibility of a forked repository.
|
||||||
|
@ -351,6 +352,9 @@ auto_init = Initialize this repository with selected files and template
|
||||||
create_repo = Create Repository
|
create_repo = Create Repository
|
||||||
default_branch = Default Branch
|
default_branch = Default Branch
|
||||||
mirror_interval = Mirror Interval (hour)
|
mirror_interval = Mirror Interval (hour)
|
||||||
|
watchers = Watchers
|
||||||
|
stargazers = Stargazers
|
||||||
|
forks = Forks
|
||||||
|
|
||||||
form.name_reserved = Repository name '%s' is reserved.
|
form.name_reserved = Repository name '%s' is reserved.
|
||||||
form.name_pattern_not_allowed = Repository name pattern '%s' is not allowed.
|
form.name_pattern_not_allowed = Repository name pattern '%s' is not allowed.
|
||||||
|
@ -361,16 +365,16 @@ migrate_type_helper = This repository will be a <span class="text blue">mirror</
|
||||||
migrate_repo = Migrate Repository
|
migrate_repo = Migrate Repository
|
||||||
migrate.clone_address = Clone Address
|
migrate.clone_address = Clone Address
|
||||||
migrate.clone_address_desc = This can be a HTTP/HTTPS/GIT URL or local server path.
|
migrate.clone_address_desc = This can be a HTTP/HTTPS/GIT URL or local server path.
|
||||||
|
migrate.permission_denied = You are not allowed to import local repositories.
|
||||||
migrate.invalid_local_path = Invalid local path, it does not exist or not a directory.
|
migrate.invalid_local_path = Invalid local path, it does not exist or not a directory.
|
||||||
|
migrate.failed = Migration failed: %v
|
||||||
|
|
||||||
forked_from = forked from
|
forked_from = forked from
|
||||||
fork_from_self = You cannot fork repository you already owned!
|
fork_from_self = You cannot fork repository you already owned!
|
||||||
copy_link = Copy
|
copy_link = Copy
|
||||||
copy_link_success = Copied!
|
copy_link_success = Copied!
|
||||||
copy_link_error = Press ⌘-C or Ctrl-C to copy
|
copy_link_error = Press ⌘-C or Ctrl-C to copy
|
||||||
click_to_copy = Copy to clipboard
|
|
||||||
copied = Copied OK
|
copied = Copied OK
|
||||||
clone_helper = Need help cloning? Visit <a target="_blank" href="%s">Help</a>!
|
|
||||||
unwatch = Unwatch
|
unwatch = Unwatch
|
||||||
watch = Watch
|
watch = Watch
|
||||||
unstar = Unstar
|
unstar = Unstar
|
||||||
|
@ -384,10 +388,9 @@ create_new_repo_command = Create a new repository on the command line
|
||||||
push_exist_repo = Push an existing repository from the command line
|
push_exist_repo = Push an existing repository from the command line
|
||||||
repo_is_empty = This repository is empty, please come back later!
|
repo_is_empty = This repository is empty, please come back later!
|
||||||
|
|
||||||
|
|
||||||
branch = Branch
|
branch = Branch
|
||||||
tree = Tree
|
tree = Tree
|
||||||
branch_and_tags = Branches & Tags
|
filter_branch_and_tag = Filter branch or tag
|
||||||
branches = Branches
|
branches = Branches
|
||||||
tags = Tags
|
tags = Tags
|
||||||
issues = Issues
|
issues = Issues
|
||||||
|
@ -456,9 +459,9 @@ issues.num_comments = %d comments
|
||||||
issues.commented_at = `commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at = `commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content = There is no content yet.
|
issues.no_content = There is no content yet.
|
||||||
issues.close_issue = Close
|
issues.close_issue = Close
|
||||||
issues.close_comment_issue = Close and comment
|
issues.close_comment_issue = Comment and close
|
||||||
issues.reopen_issue = Reopen
|
issues.reopen_issue = Reopen
|
||||||
issues.reopen_comment_issue = Reopen and comment
|
issues.reopen_comment_issue = Comment and reopen
|
||||||
issues.create_comment = Comment
|
issues.create_comment = Comment
|
||||||
issues.closed_at = `closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at = `closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at = `reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at = `reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
@ -482,6 +485,7 @@ issues.label_deletion = Label Deletion
|
||||||
issues.label_deletion_desc = Delete this label will remove its information in all related issues. Do you want to continue?
|
issues.label_deletion_desc = Delete this label will remove its information in all related issues. Do you want to continue?
|
||||||
issues.label_deletion_success = Label has been deleted successfully!
|
issues.label_deletion_success = Label has been deleted successfully!
|
||||||
|
|
||||||
|
pulls.new = New Pull Request
|
||||||
pulls.compare_changes = Compare Changes
|
pulls.compare_changes = Compare Changes
|
||||||
pulls.compare_changes_desc = Compare two branches and make a pull request for changes.
|
pulls.compare_changes_desc = Compare two branches and make a pull request for changes.
|
||||||
pulls.compare_base = base
|
pulls.compare_base = base
|
||||||
|
@ -520,7 +524,7 @@ milestones.title = Title
|
||||||
milestones.desc = Description
|
milestones.desc = Description
|
||||||
milestones.due_date = Due Date (optional)
|
milestones.due_date = Due Date (optional)
|
||||||
milestones.clear = Clear
|
milestones.clear = Clear
|
||||||
milestones.invalid_due_date_format = Due date format is invalid, must be 'year-mm-dd'.
|
milestones.invalid_due_date_format = Due date format is invalid, must be 'yyyy-mm-dd'.
|
||||||
milestones.create_success = Milestone '%s' has been created successfully!
|
milestones.create_success = Milestone '%s' has been created successfully!
|
||||||
milestones.edit = Edit Milestone
|
milestones.edit = Edit Milestone
|
||||||
milestones.edit_subheader = Use better description for milestones so people won't be confused.
|
milestones.edit_subheader = Use better description for milestones so people won't be confused.
|
||||||
|
@ -562,6 +566,7 @@ settings.confirm_delete = Confirm Deletion
|
||||||
settings.add_collaborator = Add New Collaborator
|
settings.add_collaborator = Add New Collaborator
|
||||||
settings.add_collaborator_success = New collaborator has been added.
|
settings.add_collaborator_success = New collaborator has been added.
|
||||||
settings.remove_collaborator_success = Collaborator has been removed.
|
settings.remove_collaborator_success = Collaborator has been removed.
|
||||||
|
settings.search_user_placeholder = Search user...
|
||||||
settings.user_is_org_member = User is organization member who cannot be added as a collaborator.
|
settings.user_is_org_member = User is organization member who cannot be added as a collaborator.
|
||||||
settings.add_webhook = Add Webhook
|
settings.add_webhook = Add Webhook
|
||||||
settings.hooks_desc = Webhooks are much like basic HTTP POST event triggers. Whenever something occurs in Gogs, we will handle the notification to the target host you specify. Learn more in this <a target="_blank" href="%s">Webhooks Guide</a>.
|
settings.hooks_desc = Webhooks are much like basic HTTP POST event triggers. Whenever something occurs in Gogs, we will handle the notification to the target host you specify. Learn more in this <a target="_blank" href="%s">Webhooks Guide</a>.
|
||||||
|
@ -634,24 +639,32 @@ release.stable = Stable
|
||||||
release.edit = edit
|
release.edit = edit
|
||||||
release.ahead = <strong>%d</strong> commits to %s since this release
|
release.ahead = <strong>%d</strong> commits to %s since this release
|
||||||
release.source_code = Source Code
|
release.source_code = Source Code
|
||||||
|
release.new_subheader = Publish releases to iterate product.
|
||||||
|
release.edit_subheader = Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name = Tag name
|
release.tag_name = Tag name
|
||||||
release.target = Target
|
release.target = Target
|
||||||
release.tag_helper = Choose an existing tag, or create a new tag on publish.
|
release.tag_helper = Choose an existing tag, or create a new tag on publish.
|
||||||
release.release_title = Release title
|
release.title = Title
|
||||||
release.content_with_md = Content with <a href="%s">Markdown</a>
|
release.content = Content
|
||||||
release.write = Write
|
release.write = Write
|
||||||
release.preview = Preview
|
release.preview = Preview
|
||||||
release.content_placeholder = Write some content
|
|
||||||
release.loading = Loading...
|
release.loading = Loading...
|
||||||
release.prerelease_desc = This is a pre-release
|
release.prerelease_desc = This is a pre-release
|
||||||
release.prerelease_helper = We’ll point out that this release is not production-ready.
|
release.prerelease_helper = We’ll point out that this release is not production-ready.
|
||||||
|
release.cancel = Cancel
|
||||||
release.publish = Publish Release
|
release.publish = Publish Release
|
||||||
release.save_draft = Save Draft
|
release.save_draft = Save Draft
|
||||||
release.edit_release = Edit Release
|
release.edit_release = Edit Release
|
||||||
|
release.delete_release = Delete This Release
|
||||||
|
release.deletion = Release Deletion
|
||||||
|
release.deletion_desc = Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success = Release has been deleted successfully!
|
||||||
release.tag_name_already_exist = Release with this tag name has already existed.
|
release.tag_name_already_exist = Release with this tag name has already existed.
|
||||||
|
release.downloads = Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder = Organization Name
|
org_name_holder = Organization Name
|
||||||
|
org_full_name_holder = Organization Full Name
|
||||||
org_name_helper = Great organization names are short and memorable.
|
org_name_helper = Great organization names are short and memorable.
|
||||||
create_org = Create Organization
|
create_org = Create Organization
|
||||||
repo_updated = Updated
|
repo_updated = Updated
|
||||||
|
@ -688,16 +701,17 @@ settings.delete_org_title = Organization Deletion
|
||||||
settings.delete_org_desc = This organization is going to be deleted permanently, do you want to continue?
|
settings.delete_org_desc = This organization is going to be deleted permanently, do you want to continue?
|
||||||
settings.hooks_desc = Add webhooks that will be triggered for <strong>all repositories</strong> under this organization.
|
settings.hooks_desc = Add webhooks that will be triggered for <strong>all repositories</strong> under this organization.
|
||||||
|
|
||||||
|
members.membership_visibility = Membership Visibility:
|
||||||
members.public = Public
|
members.public = Public
|
||||||
members.public_helper = make private
|
members.public_helper = make private
|
||||||
members.private = Private
|
members.private = Private
|
||||||
members.private_helper = make public
|
members.private_helper = make public
|
||||||
|
members.member_role = Member Role:
|
||||||
members.owner = Owner
|
members.owner = Owner
|
||||||
members.member = Member
|
members.member = Member
|
||||||
members.conceal = Conceal
|
|
||||||
members.remove = Remove
|
members.remove = Remove
|
||||||
members.leave = Leave
|
members.leave = Leave
|
||||||
members.invite_desc = Start typing a username to invite a new member to %s:
|
members.invite_desc = Add a new member to %s:
|
||||||
members.invite_now = Invite Now
|
members.invite_now = Invite Now
|
||||||
|
|
||||||
teams.join = Join
|
teams.join = Join
|
||||||
|
@ -722,6 +736,7 @@ teams.read_permission_desc = This team grants <strong>Read</strong> access: memb
|
||||||
teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to the team's repositories.
|
teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to the team's repositories.
|
||||||
teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to, and add collaborators to the team's repositories.
|
teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to, and add collaborators to the team's repositories.
|
||||||
teams.repositories = Team Repositories
|
teams.repositories = Team Repositories
|
||||||
|
teams.search_repo_placeholder = Search repository...
|
||||||
teams.add_team_repository = Add Team Repository
|
teams.add_team_repository = Add Team Repository
|
||||||
teams.remove_repo = Remove
|
teams.remove_repo = Remove
|
||||||
teams.add_nonexistent_repo = The repository you're trying to add does not exist, please create it first.
|
teams.add_nonexistent_repo = The repository you're trying to add does not exist, please create it first.
|
||||||
|
@ -752,6 +767,8 @@ dashboard.delete_inactivate_accounts = Delete all inactive accounts
|
||||||
dashboard.delete_inactivate_accounts_success = All inactivate accounts have been deleted successfully.
|
dashboard.delete_inactivate_accounts_success = All inactivate accounts have been deleted successfully.
|
||||||
dashboard.delete_repo_archives = Delete all repositories archives
|
dashboard.delete_repo_archives = Delete all repositories archives
|
||||||
dashboard.delete_repo_archives_success = All repositories archives have been deleted successfully.
|
dashboard.delete_repo_archives_success = All repositories archives have been deleted successfully.
|
||||||
|
dashboard.delete_missing_repos = Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success = All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos = Do garbage collection on repositories
|
dashboard.git_gc_repos = Do garbage collection on repositories
|
||||||
dashboard.git_gc_repos_success = All repositories have done garbage collection successfully.
|
dashboard.git_gc_repos_success = All repositories have done garbage collection successfully.
|
||||||
dashboard.resync_all_sshkeys = Rewrite '.ssh/authorized_keys' file (caution: non-Gogs keys will be lost)
|
dashboard.resync_all_sshkeys = Rewrite '.ssh/authorized_keys' file (caution: non-Gogs keys will be lost)
|
||||||
|
@ -808,6 +825,7 @@ users.edit_account = Edit Account
|
||||||
users.is_activated = This account is activated
|
users.is_activated = This account is activated
|
||||||
users.is_admin = This account has administrator permissions
|
users.is_admin = This account has administrator permissions
|
||||||
users.allow_git_hook = This account has permissions to create Git hooks
|
users.allow_git_hook = This account has permissions to create Git hooks
|
||||||
|
users.allow_import_local = This account has permissions to import local repositories
|
||||||
users.update_profile = Update Account Profile
|
users.update_profile = Update Account Profile
|
||||||
users.delete_account = Delete This Account
|
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_own_repo = This account still has ownership over at least one repository, you have to delete or transfer them first.
|
||||||
|
@ -955,7 +973,7 @@ notices.delete_success = System notice has been deleted successfully.
|
||||||
[action]
|
[action]
|
||||||
create_repo = created repository <a href="%s">%s</a>
|
create_repo = created repository <a href="%s">%s</a>
|
||||||
rename_repo = renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
rename_repo = renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo = pushed to <a href="%s/src/%s">%[2]s</a> at <a href="%[1]s">%[3]s</a>
|
commit_repo = pushed to <a href="%[1]s/src/%[2]s">%[3]s</a> at <a href="%[1]s">%[4]s</a>
|
||||||
create_issue = `opened issue <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue = `opened issue <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 = `created pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue = `commented on issue <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue = `commented on issue <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
|
|
|
@ -111,7 +111,7 @@ admin_title=Configuración de la Cuenta de Administrador
|
||||||
admin_name=Nombre de usuario
|
admin_name=Nombre de usuario
|
||||||
admin_password=Contraseña
|
admin_password=Contraseña
|
||||||
confirm_password=Confirmar Contraseña
|
confirm_password=Confirmar Contraseña
|
||||||
admin_email=Correo electrónico
|
admin_email=Correo electrónico del administrador
|
||||||
install_gogs=Instalar Gogs
|
install_gogs=Instalar Gogs
|
||||||
test_git_failed=Fallo al probar el comando 'git': %v
|
test_git_failed=Fallo al probar el comando 'git': %v
|
||||||
sqlite3_not_available=Tu versión no soporta SQLite3, por favor descarga el binario oficial desde %s, NO la versión de gobuild.
|
sqlite3_not_available=Tu versión no soporta SQLite3, por favor descarga el binario oficial desde %s, NO la versión de gobuild.
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=He olvidado mi contraseña
|
||||||
forget_password=¿Has olvidado tu contraseña?
|
forget_password=¿Has olvidado tu contraseña?
|
||||||
sign_up_now=¿Necesitas una cuenta? Regístrate ahora.
|
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.
|
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_to_account=Inicie sesión en su cuenta
|
|
||||||
active_your_account=Activa tu 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.
|
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.
|
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.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Por favor, active su cuenta
|
||||||
activate_email=Verifique su correo electrónico
|
activate_email=Verifique su correo electrónico
|
||||||
reset_password=Restablezca su contraseña
|
reset_password=Restablezca su contraseña
|
||||||
register_success=Registro completado, bienvenido
|
register_success=Registro completado, bienvenido
|
||||||
|
register_notify=Bienvenido a bordo
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Sí
|
yes=Sí
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` debe contener al menos %s caracteres.`
|
||||||
max_size_error=` debe contener como máximo %s caracteres.`
|
max_size_error=` debe contener como máximo %s caracteres.`
|
||||||
email_error=` no es una dirección de correo válida.`
|
email_error=` no es una dirección de correo válida.`
|
||||||
url_error=` no es una URL válida.`
|
url_error=` no es una URL válida.`
|
||||||
|
include_error='debe contener la subcadena '%s'.'
|
||||||
unknown_error=Error desconocido:
|
unknown_error=Error desconocido:
|
||||||
captcha_incorrect=El captcha no es válido.
|
captcha_incorrect=El captcha no es válido.
|
||||||
password_not_match=La contraseña de confirmación no coincide.
|
password_not_match=La contraseña de confirmación no coincide.
|
||||||
|
@ -334,8 +335,9 @@ repo_name=Nombre del Repositorio
|
||||||
repo_name_helper=Los grandes nombres de repositorios son cortos, memorables y <strong>únicos</strong>.
|
repo_name_helper=Los grandes nombres de repositorios son cortos, memorables y <strong>únicos</strong>.
|
||||||
visibility=Visibilidad
|
visibility=Visibilidad
|
||||||
visiblity_helper=Este repositorio es <span class="ui red text">Privado</span>
|
visiblity_helper=Este repositorio es <span class="ui red text">Privado</span>
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=El administrador web ha obligado a todos los repositorios nuevos a ser <span class="ui red text"> privados</span>
|
||||||
visiblity_fork_helper=(Este cambio afectará a todos los forks)
|
visiblity_fork_helper=(Este cambio afectará a todos los forks)
|
||||||
|
clone_helper=¿Necesitas ayuda con el clone? ¡Consulta la <a target="_blank" href="%s">Ayuda</a>!
|
||||||
fork_repo=Hacer Fork del repositorio
|
fork_repo=Hacer Fork del repositorio
|
||||||
fork_from=Crear un Fork desde
|
fork_from=Crear un Fork desde
|
||||||
fork_visiblity_helper=No es posible cambiar la visibilidad de un Fork
|
fork_visiblity_helper=No es posible cambiar la visibilidad de un Fork
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Inicializar los archivos seleccionados y plantillas de este repositori
|
||||||
create_repo=Crear Repositorio
|
create_repo=Crear Repositorio
|
||||||
default_branch=Rama por defecto
|
default_branch=Rama por defecto
|
||||||
mirror_interval=Intervalo de mirror(en horas)
|
mirror_interval=Intervalo de mirror(en horas)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=El nombre del repositorio '%s' está reservado.
|
form.name_reserved=El nombre del repositorio '%s' está reservado.
|
||||||
form.name_pattern_not_allowed=El patrón del nombre del repositorio '%s' no está permitido.
|
form.name_pattern_not_allowed=El patrón del nombre del repositorio '%s' no está permitido.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=Este repositorio será un <span class="text blue">mirror</sp
|
||||||
migrate_repo=Migrar Repositorio
|
migrate_repo=Migrar Repositorio
|
||||||
migrate.clone_address=Clonar Dirección
|
migrate.clone_address=Clonar Dirección
|
||||||
migrate.clone_address_desc=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.permission_denied=No te está permitido importar repositorios locales.
|
||||||
migrate.invalid_local_path=Rutal local inválida, no existe o no es un directorio.
|
migrate.invalid_local_path=Rutal local inválida, no existe o no es un directorio.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=forked de
|
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
|
copy_link=Copiar
|
||||||
copy_link_success=Copiado!
|
copy_link_success=Copiado!
|
||||||
copy_link_error=Presione ⌘ + C o Ctrl-C para copiar
|
copy_link_error=Presione ⌘ + C o Ctrl-C para copiar
|
||||||
click_to_copy=Copiar al portapapeles
|
|
||||||
copied=Copiado correctamente
|
copied=Copiado correctamente
|
||||||
clone_helper=¿Necesitas ayuda con el clone? ¡Consulta la <a target="_blank" href="%s">Ayuda</a>!
|
|
||||||
unwatch=Dejar de vigilar
|
unwatch=Dejar de vigilar
|
||||||
watch=Vigilar
|
watch=Vigilar
|
||||||
unstar=Eliminar destacado
|
unstar=Eliminar destacado
|
||||||
|
@ -381,12 +386,11 @@ quick_guide=Guía Rápida
|
||||||
clone_this_repo=Clonar este repositorio
|
clone_this_repo=Clonar este repositorio
|
||||||
create_new_repo_command=Crear un nuevo repositorio desde línea de comandos
|
create_new_repo_command=Crear un nuevo repositorio desde línea de comandos
|
||||||
push_exist_repo=Hacer Push de un repositorio existente desde línea de comandos
|
push_exist_repo=Hacer Push de un repositorio existente desde línea de comandos
|
||||||
repo_is_empty=This repository is empty, please come back later!
|
repo_is_empty=Este repositorio está vacío, por favor, ¡vuelva más tarde!
|
||||||
|
|
||||||
|
|
||||||
branch=Rama
|
branch=Rama
|
||||||
tree=Árbol
|
tree=Árbol
|
||||||
branch_and_tags=Ramas y Etiquetas
|
filter_branch_and_tag=Filtrar por rama o etiqueta
|
||||||
branches=Ramas
|
branches=Ramas
|
||||||
tags=Etiquetas
|
tags=Etiquetas
|
||||||
issues=Incidencias
|
issues=Incidencias
|
||||||
|
@ -455,9 +459,9 @@ issues.num_comments=%d comentarios
|
||||||
issues.commented_at=`comentada <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at=`comentada <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content=Aun no existe contenido.
|
issues.no_content=Aun no existe contenido.
|
||||||
issues.close_issue=Cerrar
|
issues.close_issue=Cerrar
|
||||||
issues.close_comment_issue=Cerrar y Comentar
|
issues.close_comment_issue=Comentar y cerrar
|
||||||
issues.reopen_issue=Reabrir
|
issues.reopen_issue=Reabrir
|
||||||
issues.reopen_comment_issue=Reabrir y Comentar
|
issues.reopen_comment_issue=Comentar y reabrir
|
||||||
issues.create_comment=Comentar
|
issues.create_comment=Comentar
|
||||||
issues.closed_at=`cerrada <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
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.reopened_at=`reabierta <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
@ -481,6 +485,7 @@ 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_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!
|
issues.label_deletion_success=Etiqueta borrada con éxito!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Comparar Cambios
|
pulls.compare_changes=Comparar Cambios
|
||||||
pulls.compare_changes_desc=Comparar dos ramas y generar un pull request con las diferencias.
|
pulls.compare_changes_desc=Comparar dos ramas y generar un pull request con las diferencias.
|
||||||
pulls.compare_base=base
|
pulls.compare_base=base
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=Título
|
||||||
milestones.desc=Descripción
|
milestones.desc=Descripción
|
||||||
milestones.due_date=Fecha límite (opcional)
|
milestones.due_date=Fecha límite (opcional)
|
||||||
milestones.clear=Eliminar
|
milestones.clear=Eliminar
|
||||||
milestones.invalid_due_date_format=El formato de la fecha límite no es válido, debe ser 'y-mm-dd'.
|
milestones.invalid_due_date_format=El formato de la fecha límite no es válido, debe ser 'yyyy-mm-dd'.
|
||||||
milestones.create_success=¡El milestone '%s' ha sido creado con éxito!
|
milestones.create_success=¡El milestone '%s' ha sido creado con éxito!
|
||||||
milestones.edit=Editar Milestone
|
milestones.edit=Editar Milestone
|
||||||
milestones.edit_subheader=Use una buena descripción en el milestone para no confundir al resto de usuarios.
|
milestones.edit_subheader=Use una buena descripción en el milestone para no confundir al resto de usuarios.
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Confirmar Eliminación
|
||||||
settings.add_collaborator=Añadir Nuevo Colaborador
|
settings.add_collaborator=Añadir Nuevo Colaborador
|
||||||
settings.add_collaborator_success=Se ha añadido el nuevo colaborador.
|
settings.add_collaborator_success=Se ha añadido el nuevo colaborador.
|
||||||
settings.remove_collaborator_success=Se ha eliminado el colaborador.
|
settings.remove_collaborator_success=Se ha eliminado el colaborador.
|
||||||
|
settings.search_user_placeholder=Buscar usuario...
|
||||||
settings.user_is_org_member=El usuario es miembro de la organización, no puede ser añadido como colaborador.
|
settings.user_is_org_member=El usuario es miembro de la organización, no puede ser añadido como colaborador.
|
||||||
settings.add_webhook=Añadir Webhook
|
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.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>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Estable
|
||||||
release.edit=editar
|
release.edit=editar
|
||||||
release.ahead=<strong>%d</strong> commits en %s desde esta release
|
release.ahead=<strong>%d</strong> commits en %s desde esta release
|
||||||
release.source_code=Código Fuente
|
release.source_code=Código Fuente
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Nombre de la etiqueta
|
release.tag_name=Nombre de la etiqueta
|
||||||
release.target=Destino
|
release.target=Destino
|
||||||
release.tag_helper=Escoge una etiqueta o crea una nueva al publicar.
|
release.tag_helper=Escoge una etiqueta o crea una nueva al publicar.
|
||||||
release.release_title=Título de la Release
|
release.title=Título
|
||||||
release.content_with_md=Contenido con formato <a href="%s">Markdown</a>
|
release.content=Contenido
|
||||||
release.write=Escribir
|
release.write=Escribir
|
||||||
release.preview=Vista Previa
|
release.preview=Vista Previa
|
||||||
release.content_placeholder=Escribe algo de contenido
|
|
||||||
release.loading=Cargando...
|
release.loading=Cargando...
|
||||||
release.prerelease_desc=Esta es una pre-release
|
release.prerelease_desc=Esta es una pre-release
|
||||||
release.prerelease_helper=Esta release está marcada como no apta para producción.
|
release.prerelease_helper=Esta release está marcada como no apta para producción.
|
||||||
|
release.cancel=Cancelar
|
||||||
release.publish=Publicar Release
|
release.publish=Publicar Release
|
||||||
release.save_draft=Guardar Borrador
|
release.save_draft=Guardar Borrador
|
||||||
release.edit_release=Editar Release
|
release.edit_release=Editar Release
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Ya existe una Release con esta etiqueta.
|
release.tag_name_already_exist=Ya existe una Release con esta etiqueta.
|
||||||
|
release.downloads=Descargas
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Nombre de la Organización
|
org_name_holder=Nombre de la Organización
|
||||||
|
org_full_name_holder=Nombre de la organización
|
||||||
org_name_helper=Los grandes nombres de organizaciones son cortos y memorables.
|
org_name_helper=Los grandes nombres de organizaciones son cortos y memorables.
|
||||||
create_org=Crear Organización
|
create_org=Crear Organización
|
||||||
repo_updated=Actualizado
|
repo_updated=Actualizado
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=Eliminar todas las cuentas inactivas
|
||||||
dashboard.delete_inactivate_accounts_success=Todas las cuentas inactivas se han eliminado correctamente.
|
dashboard.delete_inactivate_accounts_success=Todas las cuentas inactivas se han eliminado correctamente.
|
||||||
dashboard.delete_repo_archives=Eliminar todos los archivos de repositorios
|
dashboard.delete_repo_archives=Eliminar todos los archivos de repositorios
|
||||||
dashboard.delete_repo_archives_success=Todos los archivos de repositorios se han eliminado correctamente.
|
dashboard.delete_repo_archives_success=Todos los archivos de repositorios se han eliminado correctamente.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Ejecutar la recolección de basura en los repositorios
|
dashboard.git_gc_repos=Ejecutar la recolección de basura en los repositorios
|
||||||
dashboard.git_gc_repos_success=Todos los repositorios han ejecutado correctamente el recolector de basuras.
|
dashboard.git_gc_repos_success=Todos los repositorios han ejecutado correctamente el recolector de basuras.
|
||||||
dashboard.resync_all_sshkeys=Reescribir el fichero '.ssh/authorized_keys'(atención: se perderán las claves que no pertenezcan a Gogs)
|
dashboard.resync_all_sshkeys=Reescribir el fichero '.ssh/authorized_keys'(atención: se perderán las claves que no pertenezcan a Gogs)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Editar Cuenta
|
||||||
users.is_activated=Esta cuenta está activada
|
users.is_activated=Esta cuenta está activada
|
||||||
users.is_admin=Esta cuenta tiene permisos de administrador
|
users.is_admin=Esta cuenta tiene permisos de administrador
|
||||||
users.allow_git_hook=Esta cuenta tiene permisos para crear hooks de Git
|
users.allow_git_hook=Esta cuenta tiene permisos para crear hooks de Git
|
||||||
|
users.allow_import_local=Esta cuenta dispone de permisos para importar repositorios locales
|
||||||
users.update_profile=Actualizar Perfil de la Cuenta
|
users.update_profile=Actualizar Perfil de la Cuenta
|
||||||
users.delete_account=Eliminar esta 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_own_repo=Esta cuenta es propietaria de uno o más repositorios, tienes que borrarlos o transferirlos primero.
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=La notificación del sistema se ha eliminado correctament
|
||||||
[action]
|
[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>
|
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>
|
commit_repo=hizo push a <a href="%[1]s/src/%[2]s">%[3]s</a> en <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`incidencia abierta <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`incidencia abierta <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
create_pull_request=`creado pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`creado pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`comentó en la incidencia <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue=`comentó en la incidencia <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
|
@ -967,20 +984,20 @@ compare_2_commits=Ver la comparación de estos 2 commits
|
||||||
ago=hace
|
ago=hace
|
||||||
from_now=desde ahora
|
from_now=desde ahora
|
||||||
now=ahora
|
now=ahora
|
||||||
1s=1 segundo %s
|
1s=%s 1 segundo
|
||||||
1m=1 minuto %s
|
1m=%s 1 minuto
|
||||||
1h=1 hora %s
|
1h=%s 1 hora
|
||||||
1d=1 día %s
|
1d=%s 1 día
|
||||||
1w=1 semana %s
|
1w=%s 1 semana
|
||||||
1mon=1 mes %s
|
1mon=%s 1 mes
|
||||||
1y=1 año %s
|
1y=%s 1 año
|
||||||
seconds=%d segundos %s
|
seconds=%[2]d %[1]s segundos
|
||||||
minutes=%d minutos %s
|
minutes=%s %d minutos
|
||||||
hours=%d horas %s
|
hours=%s %d horas
|
||||||
days=%d días %s
|
days=%s %d días
|
||||||
weeks=%d semanas %s
|
weeks=%s %d semanas
|
||||||
months=%d meses %s
|
months=%s %d meses
|
||||||
years=%d años %s
|
years=%s %d años
|
||||||
raw_seconds=segundos
|
raw_seconds=segundos
|
||||||
raw_minutes=minutos
|
raw_minutes=minutos
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
app_desc=Un service de Git sans prise de tête auto-hébergé codé en Go
|
app_desc=Un service Git auto-hébergé sans prise de tête
|
||||||
|
|
||||||
home=Accueil
|
home=Accueil
|
||||||
dashboard=Tableau de bord
|
dashboard=Tableau de bord
|
||||||
|
@ -7,7 +7,7 @@ help=Aide
|
||||||
sign_in=Connexion
|
sign_in=Connexion
|
||||||
sign_out=Déconnexion
|
sign_out=Déconnexion
|
||||||
sign_up=Créer un compte
|
sign_up=Créer un compte
|
||||||
register=S'inscrire
|
register=Inscription
|
||||||
website=Site Web
|
website=Site Web
|
||||||
version=Version
|
version=Version
|
||||||
page=Page
|
page=Page
|
||||||
|
@ -53,7 +53,7 @@ code=Code
|
||||||
[install]
|
[install]
|
||||||
install=Installation
|
install=Installation
|
||||||
title=Instructions pour la première exécution
|
title=Instructions pour la première exécution
|
||||||
docker_helper=Si vous exécuté Gogs grâce à Docker, merci de lire la <a target="_blank" href="%s">procédure</a> attentivement avant de modifier quoi que ce soit dans cette page !
|
docker_helper=Si vous exécutez Gogs grâce à Docker, merci de lire la <a target="_blank" href="%s">procédure</a> attentivement avant de modifier quoi que ce soit sur cette page !
|
||||||
requite_db_desc=Gogs requiert MySQL, PostgreSQL, SQLite3 ou TiDB.
|
requite_db_desc=Gogs requiert MySQL, PostgreSQL, SQLite3 ou TiDB.
|
||||||
db_title=Paramètres de la base de données
|
db_title=Paramètres de la base de données
|
||||||
db_type=Type de Base de Données
|
db_type=Type de Base de Données
|
||||||
|
@ -66,9 +66,9 @@ ssl_mode=Mode SSL
|
||||||
path=Chemin
|
path=Chemin
|
||||||
sqlite_helper=Le chemin du fichier de la base de données SQLite3 ou TiDB.
|
sqlite_helper=Le chemin du fichier de la base de données SQLite3 ou TiDB.
|
||||||
err_empty_db_path=Le chemin de la base de données SQLite3 ou TiDB ne peut être vide.
|
err_empty_db_path=Le chemin de la base de données SQLite3 ou TiDB ne peut être vide.
|
||||||
err_invalid_tidb_name=Le nom de la base de données TIDB ne peux contenir les caractères "." ou "-".
|
err_invalid_tidb_name=Le nom de la base de données TiDB ne peut contenir les caractères "." ou "-".
|
||||||
no_admin_and_disable_registration=Vous ne pouvez pas désactiver l'enregistrement sans créer un compte admin.
|
no_admin_and_disable_registration=Vous ne pouvez pas désactiver l'enregistrement sans créer un compte administrateur.
|
||||||
err_empty_admin_password=Le mot de passe Admin ne peut pas être vide.
|
err_empty_admin_password=Le mot de passe du compte administrateur ne peut être vide.
|
||||||
|
|
||||||
general_title=Paramètres Généraux de Gogs
|
general_title=Paramètres Généraux de Gogs
|
||||||
app_name=Nom de l'Application
|
app_name=Nom de l'Application
|
||||||
|
@ -80,7 +80,7 @@ run_user_helper=L'utilisateur doit avoir accès à la Racine du Référentiel et
|
||||||
domain=Domaine
|
domain=Domaine
|
||||||
domain_helper=Cela affecte les doublons d'URL SSH.
|
domain_helper=Cela affecte les doublons d'URL SSH.
|
||||||
ssh_port=Port SSH
|
ssh_port=Port SSH
|
||||||
ssh_port_helper=C'est le numéro de port qui qui est utilisé par votre serveur SSH, le laisser vide pour désactiver la fonctionnalité.
|
ssh_port_helper=C'est le numéro de port qui est utilisé par votre serveur SSH, le laisser vide pour désactiver la fonctionnalité.
|
||||||
http_port=Port HTTP
|
http_port=Port HTTP
|
||||||
http_port_helper=Numéro de port que l'application écoutera.
|
http_port_helper=Numéro de port que l'application écoutera.
|
||||||
app_url=URL de l'Application
|
app_url=URL de l'Application
|
||||||
|
@ -106,19 +106,19 @@ enable_captcha=Activez le Captcha
|
||||||
enable_captcha_popup=Demande la validation Captcha pour l'auto-enregistrement de l'utilisateur.
|
enable_captcha_popup=Demande la validation Captcha pour l'auto-enregistrement de l'utilisateur.
|
||||||
require_sign_in_view=Demander une connexion pour afficher des pages
|
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.
|
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.
|
admin_setting_desc=Vous n'avez pas besoin de créer un compte admin. L'utilisateur ayant l'ID = 1 se verra automatiquement attribuer l'accès administrateur.
|
||||||
admin_title=Paramètres du Compte Administrateur
|
admin_title=Paramètres du Compte Administrateur
|
||||||
admin_name=Nom d'Utilisateur
|
admin_name=Nom d'Utilisateur
|
||||||
admin_password=Mot de Passe
|
admin_password=Mot de Passe
|
||||||
confirm_password=Confirmez le Mot de Passe
|
confirm_password=Confirmez le Mot de Passe
|
||||||
admin_email=E-mail
|
admin_email=E-mail de l'administrateur
|
||||||
install_gogs=Installer Gogs
|
install_gogs=Installer Gogs
|
||||||
test_git_failed=Tentative de commande "git" échouée : %v
|
test_git_failed=Tentative de commande "git" échouée : %v
|
||||||
sqlite3_not_available=Votre version publiée ne prend pas en charge SQLite3. Veuillez télécharger la version binaire officielle à cette adresse %s.
|
sqlite3_not_available=Votre version publiée ne prend pas en charge SQLite3. Veuillez télécharger la version binaire officielle à cette adresse %s.
|
||||||
invalid_db_setting=Paramètres de base de données incorrects : %v
|
invalid_db_setting=Paramètres de base de données incorrects : %v
|
||||||
invalid_repo_path=Chemin vers le répertoire racine invalide : %v
|
invalid_repo_path=Chemin vers le répertoire racine invalide : %v
|
||||||
run_user_not_match=L'utilisateur entré n'est pas l'utilisateur actuel : %s -> %s
|
run_user_not_match=L'utilisateur entré n'est pas l'utilisateur actuel : %s -> %s
|
||||||
save_config_failed=Sauvegarde de la configuration échouée : %v
|
save_config_failed=La sauvegarde de la configuration a échoué : %v
|
||||||
invalid_admin_setting=Paramètres du compte administrateur invalides : %v
|
invalid_admin_setting=Paramètres du compte administrateur invalides : %v
|
||||||
install_success=Bienvenue ! Nous sommes heureux que vous ayez choisi Gogs, amusez-vous et prenez soin de vous.
|
install_success=Bienvenue ! Nous sommes heureux que vous ayez choisi Gogs, amusez-vous et prenez soin de vous.
|
||||||
|
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Mot de Passe oublié
|
||||||
forget_password=Mot de Passe oublié ?
|
forget_password=Mot de Passe oublié ?
|
||||||
sign_up_now=Pas de compte ? Créer maintenant.
|
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.
|
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_to_account=Connectez-vous à votre compte
|
|
||||||
active_your_account=Activer votre Compte
|
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.
|
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.
|
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.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Veuillez activer votre compte
|
||||||
activate_email=Veuillez vérifier votre adresse e-mail
|
activate_email=Veuillez vérifier votre adresse e-mail
|
||||||
reset_password=Réinitialiser votre mot de passe
|
reset_password=Réinitialiser votre mot de passe
|
||||||
register_success=Succès de l'enregistrement, Bienvenue
|
register_success=Succès de l'enregistrement, Bienvenue
|
||||||
|
register_notify=Bienvenue à bord
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Oui
|
yes=Oui
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` %s caractères minimum `
|
||||||
max_size_error=` %s caractères maximum `
|
max_size_error=` %s caractères maximum `
|
||||||
email_error=` adresse e-mail invalide `
|
email_error=` adresse e-mail invalide `
|
||||||
url_error=` URL invalide `
|
url_error=` URL invalide `
|
||||||
|
include_error=`doit contenir la sous-chaîne '%s'.`
|
||||||
unknown_error=Erreur inconnue :
|
unknown_error=Erreur inconnue :
|
||||||
captcha_incorrect=Le Captcha ne correspond pas.
|
captcha_incorrect=Le Captcha ne correspond pas.
|
||||||
password_not_match=Le mot de passe et la confirmation de mot de passe ne correspondent pas.
|
password_not_match=Le mot de passe et la confirmation de mot de passe ne correspondent pas.
|
||||||
|
@ -246,7 +247,7 @@ uid=ID d'Utilisateur
|
||||||
|
|
||||||
public_profile=Profil Public
|
public_profile=Profil Public
|
||||||
profile_desc=Votre adresse e-mail est publique et sera utilisée pour les notifications relatives au compte, ainsi que pour toute opération Web effectuée via le site.
|
profile_desc=Votre adresse e-mail est publique et sera utilisée pour les notifications relatives au compte, ainsi que pour toute opération Web effectuée via le site.
|
||||||
full_name=Non Complet
|
full_name=Nom Complet
|
||||||
website=Site Web
|
website=Site Web
|
||||||
location=Localisation
|
location=Localisation
|
||||||
update_profile=Valider les modifications
|
update_profile=Valider les modifications
|
||||||
|
@ -259,7 +260,7 @@ cancel=Annuler
|
||||||
enable_custom_avatar=Activer l'Avatar personnalisé
|
enable_custom_avatar=Activer l'Avatar personnalisé
|
||||||
enable_custom_avatar_helper=Cette option désactive l'affichage via Gravatar
|
enable_custom_avatar_helper=Cette option désactive l'affichage via Gravatar
|
||||||
choose_new_avatar=Sélectionner un nouvel avatar
|
choose_new_avatar=Sélectionner un nouvel avatar
|
||||||
update_avatar=Mettre l'Avatar à Jour
|
update_avatar=Mettre à jour l'avatar
|
||||||
uploaded_avatar_not_a_image=Le fichier téléchargé n'est pas une image.
|
uploaded_avatar_not_a_image=Le fichier téléchargé n'est pas une image.
|
||||||
no_custom_avatar_available=Aucun avatar personnalisé disponible, activation impossible.
|
no_custom_avatar_available=Aucun avatar personnalisé disponible, activation impossible.
|
||||||
update_avatar_success=Votre avatar a été mis à jour avec succès.
|
update_avatar_success=Votre avatar a été mis à jour avec succès.
|
||||||
|
@ -313,14 +314,14 @@ unbind_success=Compte de réseau social dissocié.
|
||||||
manage_access_token=Gérer les jetons d'accès personnels
|
manage_access_token=Gérer les jetons d'accès personnels
|
||||||
generate_new_token=Générer le nouveau jeton
|
generate_new_token=Générer le nouveau jeton
|
||||||
tokens_desc=Jetons, que vous avez généré, qui peuvent être utilisés pour accéder à l'API Gogs.
|
tokens_desc=Jetons, que vous avez généré, qui peuvent être utilisés pour accéder à l'API Gogs.
|
||||||
new_token_desc=Comme pour l'instant, chaque jeton aura un accès complet à votre compte.
|
new_token_desc=Chaque Jeton donnera un accès complet à votre compte.
|
||||||
token_name=Nom du jeton
|
token_name=Nom du jeton
|
||||||
generate_token=Générer le jeton
|
generate_token=Générer le jeton
|
||||||
generate_token_succees=Nouveau jeton d'accès a été généré avec succès ! Assurez-vous de copier votre nouveau jeton d'accès personnel maintenant. Vous ne serez pas en mesure de le revoir !
|
generate_token_succees=Nouveau jeton d'accès a été généré avec succès ! Assurez-vous de copier votre nouveau jeton d'accès personnel maintenant. Vous ne serez pas en mesure de le revoir !
|
||||||
delete_token=Supprimer
|
delete_token=Supprimer
|
||||||
access_token_deletion=Suppression du jeton d'accès
|
access_token_deletion=Suppression du jeton d'accès
|
||||||
access_token_deletion_desc=Supprimer ce jeton d'accès supprimera tous les accès de l'application. Voulez-vous continuer ?
|
access_token_deletion_desc=Supprimer ce jeton d'accès supprimera tous les accès de l'application. Voulez-vous continuer ?
|
||||||
delete_token_success=Le jeton d'accèsa été supprimée avec succès ! N'oubliez pas de mettre aussi à jour vos applications.
|
delete_token_success=Le jeton d'accèsa été supprimé avec succès ! N'oubliez pas de mettre à jour vos applications.
|
||||||
|
|
||||||
delete_account=Supprimer le Compte
|
delete_account=Supprimer le Compte
|
||||||
delete_prompt=Votre compte sera supprimé définitivement et cette opération est <strong>IRRÉVERSIBLE</strong> !
|
delete_prompt=Votre compte sera supprimé définitivement et cette opération est <strong>IRRÉVERSIBLE</strong> !
|
||||||
|
@ -334,11 +335,12 @@ repo_name=Nom du Référentiel
|
||||||
repo_name_helper=Idéalement, le nom d'un dépot devrait être court, mémorable et <strong>unique</strong>.
|
repo_name_helper=Idéalement, le nom d'un dépot devrait être court, mémorable et <strong>unique</strong>.
|
||||||
visibility=Visibilité
|
visibility=Visibilité
|
||||||
visiblity_helper=Ce dépôt est <span class="ui red text"> privé</span>
|
visiblity_helper=Ce dépôt est <span class="ui red text"> privé</span>
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=L'administrateur du site a forcé tous les nouveaux dépôts à être <span class="ui red text">privés</span>
|
||||||
visiblity_fork_helper=(Les changement de cette valeur affectera tous les forks)
|
visiblity_fork_helper=(Les changement de cette valeur affectera tous les forks)
|
||||||
|
clone_helper=Besoin d'aide pour dupliquer ? Visitez <a target="_blank" href="%s">l'aide</a> !
|
||||||
fork_repo=Scinder le dépôt
|
fork_repo=Scinder le dépôt
|
||||||
fork_from=Embranchement de
|
fork_from=Embranchement de
|
||||||
fork_visiblity_helper=Un dépôt scindé ne peut pas changer sa visiblité
|
fork_visiblity_helper=La visibilité d'un référentiel d'embranchement ne peut pas être modifiée.
|
||||||
repo_desc=Description
|
repo_desc=Description
|
||||||
repo_lang=Langue
|
repo_lang=Langue
|
||||||
repo_lang_helper=Sélectionnez les fichiers .gitignore
|
repo_lang_helper=Sélectionnez les fichiers .gitignore
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Initialiser ce dépôt avec le modèle et les fichiers sélectionnés
|
||||||
create_repo=Créer un dépôt
|
create_repo=Créer un dépôt
|
||||||
default_branch=Branche par défaut
|
default_branch=Branche par défaut
|
||||||
mirror_interval=Intervalle du miroir (heure)
|
mirror_interval=Intervalle du miroir (heure)
|
||||||
|
watchers=Observateurs
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Le nom de dépôt '%s' est réservé.
|
form.name_reserved=Le nom de dépôt '%s' est réservé.
|
||||||
form.name_pattern_not_allowed=Motif '%s' interdit pour les noms de dépôt.
|
form.name_pattern_not_allowed=Motif '%s' interdit pour les noms de dépôt.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=Ce dépôt sera un <span class="text blue"> miroir</span>
|
||||||
migrate_repo=Migrer le dépôt
|
migrate_repo=Migrer le dépôt
|
||||||
migrate.clone_address=Adresse du clone
|
migrate.clone_address=Adresse du clone
|
||||||
migrate.clone_address_desc=Cela peut être une URL HTTP/HTTPS/GIT ou un chemin d'accès local.
|
migrate.clone_address_desc=Cela peut être une URL HTTP/HTTPS/GIT ou un chemin d'accès local.
|
||||||
|
migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts locaux.
|
||||||
migrate.invalid_local_path=Chemin local non valide, non existant ou n'étant pas un dossier.
|
migrate.invalid_local_path=Chemin local non valide, non existant ou n'étant pas un dossier.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=dérivé depuis
|
forked_from=embranché à partir de
|
||||||
fork_from_self=Vous nous ne pouvez pas scinder un dépôt que vous possédez déja !
|
fork_from_self=Vous nous ne pouvez pas scinder un dépôt que vous possédez déja !
|
||||||
copy_link=Copier
|
copy_link=Copier
|
||||||
copy_link_success=Copié!
|
copy_link_success=Copié!
|
||||||
copy_link_error=Appuyez sur ⌘-C ou Ctrl-C pour copier
|
copy_link_error=Appuyez sur ⌘-C ou Ctrl-C pour copier
|
||||||
click_to_copy=Copier dans le presse-papiers
|
|
||||||
copied=Copié
|
copied=Copié
|
||||||
clone_helper=Besoin d'aide pour le clonage ? Visitez <a target="_blank" href="%s"> l'aider</a> !
|
|
||||||
unwatch=Ne plus suivre
|
unwatch=Ne plus suivre
|
||||||
watch=Suivre
|
watch=Suivre
|
||||||
unstar=Retirer le vote
|
unstar=Retirer le vote
|
||||||
|
@ -380,13 +385,12 @@ no_desc=Aucune description
|
||||||
quick_guide=Introduction rapide
|
quick_guide=Introduction rapide
|
||||||
clone_this_repo=Cloner ce dépôt
|
clone_this_repo=Cloner ce dépôt
|
||||||
create_new_repo_command=Créer un nouveau dépôt en ligne de commande
|
create_new_repo_command=Créer un nouveau dépôt en ligne de commande
|
||||||
push_exist_repo=Soumettre un dépôt existant par ligne de commande
|
push_exist_repo=Soumettre un référentiel existant via la ligne de commande
|
||||||
repo_is_empty=Ce référentiel est vide, veuillez revenir plus tard !
|
repo_is_empty=Ce référentiel est vide, veuillez revenir plus tard !
|
||||||
|
|
||||||
|
|
||||||
branch=Branche
|
branch=Branche
|
||||||
tree=Aborescence
|
tree=Aborescence
|
||||||
branch_and_tags=Branches & Tags
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Branches
|
branches=Branches
|
||||||
tags=Tags
|
tags=Tags
|
||||||
issues=Problèmes
|
issues=Problèmes
|
||||||
|
@ -455,13 +459,13 @@ issues.num_comments=%d commentaires
|
||||||
issues.commented_at='commenté à <a id="%[1]s" href="#%[1]s"> %[2]s'</a>
|
issues.commented_at='commenté à <a id="%[1]s" href="#%[1]s"> %[2]s'</a>
|
||||||
issues.no_content=Il n'existe pas encore de contenu.
|
issues.no_content=Il n'existe pas encore de contenu.
|
||||||
issues.close_issue=Fermer
|
issues.close_issue=Fermer
|
||||||
issues.close_comment_issue=Fermer et commenter
|
issues.close_comment_issue=Commenter et fermer
|
||||||
issues.reopen_issue=Réouvrir
|
issues.reopen_issue=Réouvrir
|
||||||
issues.reopen_comment_issue=Réouvrir et commenter
|
issues.reopen_comment_issue=Commenter et réouvrir
|
||||||
issues.create_comment=Créer un commentaire
|
issues.create_comment=Créer un commentaire
|
||||||
issues.closed_at="fermé à <a id="%[1]s"href="#%[1]s"> %[2]s"</a>
|
issues.closed_at="fermé à <a id="%[1]s"href="#%[1]s"> %[2]s"</a>
|
||||||
issues.reopened_at='réouvert à <a id="%[1]s" href="#%[1]s"> %[2]s'</a>
|
issues.reopened_at='réouvert à <a id="%[1]s" href="#%[1]s"> %[2]s'</a>
|
||||||
issues.commit_ref_at=`cette question référencé à partir d'un commit <a id="%[1]s" href="#%[1]s"> %[2]s</a>`
|
issues.commit_ref_at=`a référencé ce problème à partir d'un commit <a id="%[1]s" href="#%[1]s"> %[2]s</a>`
|
||||||
issues.poster=Publier
|
issues.poster=Publier
|
||||||
issues.admin=Admin
|
issues.admin=Admin
|
||||||
issues.owner=Propriétaire
|
issues.owner=Propriétaire
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=Suppression du Label
|
||||||
issues.label_deletion_desc=Cette opération supprimera également toutes les informations relatives aux problèmes. Voulez-vous continuer ?
|
issues.label_deletion_desc=Cette opération supprimera également toutes les informations relatives aux problèmes. Voulez-vous continuer ?
|
||||||
issues.label_deletion_success=Label supprimé avec succès !
|
issues.label_deletion_success=Label supprimé avec succès !
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Comparer les changements
|
pulls.compare_changes=Comparer les changements
|
||||||
pulls.compare_changes_desc=Comparer deux branches et faire une demande de récupération Pull pour les changements.
|
pulls.compare_changes_desc=Comparer deux branches et faire une demande de récupération Pull pour les changements.
|
||||||
pulls.compare_base=Base
|
pulls.compare_base=Base
|
||||||
|
@ -499,12 +504,12 @@ pulls.reopen_to_merge=Veuillez rouvrir cette demande de Pull Request pour effect
|
||||||
pulls.merged=Fusionné
|
pulls.merged=Fusionné
|
||||||
pulls.has_merged=Cette Pull Request a été fusionnée avec succès !
|
pulls.has_merged=Cette Pull Request a été fusionnée avec succès !
|
||||||
pulls.data_broken=Les données de cette Pull Request a été rompues en raison de la suppression d'informations du Fork.
|
pulls.data_broken=Les données de cette Pull Request a été rompues en raison de la suppression d'informations du Fork.
|
||||||
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
|
pulls.is_checking=La recherche de conflits est toujours en cours, veuillez rafraichir la page dans quelques instants.
|
||||||
pulls.can_auto_merge_desc=Vous pouvez effectuer d'opération de fusion automatique sur cette demande de Pull Request.
|
pulls.can_auto_merge_desc=Vous pouvez effectuer d'opération de fusion automatique sur cette demande de Pull Request.
|
||||||
pulls.cannot_auto_merge_desc=Vous ne pouvez effectuer des opération de fusion automatique car il y a des conflits entre les Commits.
|
pulls.cannot_auto_merge_desc=Vous ne pouvez effectuer des opération de fusion automatique car il y a des conflits entre les Commits.
|
||||||
pulls.cannot_auto_merge_helper=Veuillez utiliser l'outil en ligne de commande pour le résoudre.
|
pulls.cannot_auto_merge_helper=Veuillez utiliser l'outil en ligne de commande pour le résoudre.
|
||||||
pulls.merge_pull_request=Fusionner la Pull Request
|
pulls.merge_pull_request=Fusionner la Pull Request
|
||||||
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
|
pulls.open_unmerged_pull_exists=`Vous ne pouvez effectuer une réouverture car il y a déjà une pull-request ouverte (#%d) depuis le même dépôt avec les mêmes informations de fusion et est en attente de fusion.`
|
||||||
|
|
||||||
milestones.new=Nouveau Jalon
|
milestones.new=Nouveau Jalon
|
||||||
milestones.open_tab=%d Ouvert
|
milestones.open_tab=%d Ouvert
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=Titre
|
||||||
milestones.desc=Description
|
milestones.desc=Description
|
||||||
milestones.due_date=Date d'échéance (facultatif)
|
milestones.due_date=Date d'échéance (facultatif)
|
||||||
milestones.clear=Effacer
|
milestones.clear=Effacer
|
||||||
milestones.invalid_due_date_format=Le format de la date d'échéance est invalide, il doit être comme suit 'année-mm-jj'.
|
milestones.invalid_due_date_format=Le format de la date d'échéance est invalide, il doit être comme suit 'AAAA-mm-jj'.
|
||||||
milestones.create_success=Le Jalon '%s' a été crée avec succès !
|
milestones.create_success=Le Jalon '%s' a été crée avec succès !
|
||||||
milestones.edit=Éditer le Jalon
|
milestones.edit=Éditer le Jalon
|
||||||
milestones.edit_subheader=Utilisez une description claire pour les jalons pour ne pas induire les gens en erreur.
|
milestones.edit_subheader=Utilisez une description claire pour les jalons pour ne pas induire les gens en erreur.
|
||||||
|
@ -544,13 +549,13 @@ settings.transfer=Transférer les propriétés
|
||||||
settings.transfer_desc=Transfèrer ce dépôt à un autre utilisateur ou une organisation dont vous possédez des droits d'administrateur.
|
settings.transfer_desc=Transfèrer ce dépôt à un autre utilisateur ou une organisation dont vous possédez des droits d'administrateur.
|
||||||
settings.new_owner_has_same_repo=Le nouveau propriétaire a déjà un dépôt nommé ainsi.
|
settings.new_owner_has_same_repo=Le nouveau propriétaire a déjà un dépôt nommé ainsi.
|
||||||
settings.delete=Supprimer ce Référentiel
|
settings.delete=Supprimer ce Référentiel
|
||||||
settings.delete_desc=Attention, action irréversible. Soyez sûre de vous.
|
settings.delete_desc=Attention, action irréversible. Soyez sûr de vous.
|
||||||
settings.transfer_notices_1=-Vous perdrez l'accès si le nouveau propriétaire est un utilisateur individuel.
|
settings.transfer_notices_1=-Vous perdrez l'accès si le nouveau propriétaire est un utilisateur individuel.
|
||||||
settings.transfer_notices_2=-Vous préserverez l'accès si le nouveau propriétaire est une organisation et si vous y appartenez.
|
settings.transfer_notices_2=-Vous préserverez l'accès si le nouveau propriétaire est une organisation et si vous y appartenez.
|
||||||
settings.transfer_form_title=Veuillez recopier le texte suivant afin de confirmer votre opération :
|
settings.transfer_form_title=Veuillez recopier le texte suivant afin de confirmer votre opération :
|
||||||
settings.delete_notices_1=- Cette opération <strong>ne peut pas </strong> être annulée.
|
settings.delete_notices_1=- Cette opération <strong>ne peut pas </strong> être annulée.
|
||||||
settings.delete_notices_2=-Cette opération supprimera définitivement le dépôt, y compris les données Git, problèmes, commentaires et accès des collaborateurs.
|
settings.delete_notices_2=-Cette opération supprimera définitivement le dépôt, y compris les données Git, problèmes, commentaires et accès des collaborateurs.
|
||||||
settings.delete_notices_fork_1=-Si ce dépôt est public, tous les Forks vont devenir indépendant après la suppression.
|
settings.delete_notices_fork_1=-Si ce dépôt est public, tous les Forks vont devenir indépendants après la suppression.
|
||||||
settings.delete_notices_fork_2=-Si ce dépôt est privé, tous les Forks seront supprimés en même temps.
|
settings.delete_notices_fork_2=-Si ce dépôt est privé, tous les Forks seront supprimés en même temps.
|
||||||
settings.delete_notices_fork_3=-Si vous souhaitez conserver tous les forks après suppression, veuillez tout d'abord modifier la visibilité de ce dépôt en public.
|
settings.delete_notices_fork_3=-Si vous souhaitez conserver tous les forks après suppression, veuillez tout d'abord modifier la visibilité de ce dépôt en public.
|
||||||
settings.update_settings_success=Options mises à jour avec succès.
|
settings.update_settings_success=Options mises à jour avec succès.
|
||||||
|
@ -561,12 +566,13 @@ settings.confirm_delete=Confirmer la suppression
|
||||||
settings.add_collaborator=Ajouter un collaborateur
|
settings.add_collaborator=Ajouter un collaborateur
|
||||||
settings.add_collaborator_success=Nouveau collaborateur ajouté.
|
settings.add_collaborator_success=Nouveau collaborateur ajouté.
|
||||||
settings.remove_collaborator_success=Collaborateur supprimé.
|
settings.remove_collaborator_success=Collaborateur supprimé.
|
||||||
|
settings.search_user_placeholder=Rechercher un utilisateur...
|
||||||
settings.user_is_org_member=Cet utilisateur ne peut pas être ajouté en tant que collaborateur car il fait partie d'une organisation.
|
settings.user_is_org_member=Cet utilisateur ne peut pas être ajouté en tant que collaborateur car il fait partie d'une organisation.
|
||||||
settings.add_webhook=Ajouter un Webhook
|
settings.add_webhook=Ajouter un Webhook
|
||||||
settings.hooks_desc=Webhooks aux services externes être notifié lorsque certains événements se produisent sur Gogs. Lorsque les événements spécifiés se produisent, nous vous enverrons une demande POST à chacun des URL que vous fournissez. Apprenez-en davantage dans notre <a target="_blank" href="%s"> Webhooks Guide</a>.
|
settings.hooks_desc=Les Webhooks sont des déclencheurs de POST HTTP . Lorsque qu'un événement se produit dans Gogs, une notification sera envoyée vers l'hôte cible préalablement spécifié. Apprenez-en davantage dans le <a target="_blank" href="%s">Guide des Webhooks</a>.
|
||||||
settings.webhook_deletion=Supprimer le Webhook
|
settings.webhook_deletion=Supprimer le Webhook
|
||||||
settings.webhook_deletion_desc=Supprimer ce webhook va supprimer ses informations et l'historique de livraison. Voulez-vous continuer ?
|
settings.webhook_deletion_desc=Supprimer ce webhook va supprimer ses informations et l'historique de livraison. Voulez-vous continuer ?
|
||||||
settings.webhook_deletion_success=Le webhook a été supprimée avec succès !
|
settings.webhook_deletion_success=Le webhook a été supprimé avec succès !
|
||||||
settings.webhook.request=Requête
|
settings.webhook.request=Requête
|
||||||
settings.webhook.response=Réponse
|
settings.webhook.response=Réponse
|
||||||
settings.webhook.headers=Entêtes
|
settings.webhook.headers=Entêtes
|
||||||
|
@ -577,7 +583,7 @@ settings.githook_edit_desc=Si un Hook est inactif, un exemple de contenu vous se
|
||||||
settings.githook_name=Nom du Hook
|
settings.githook_name=Nom du Hook
|
||||||
settings.githook_content=Contenu du Hook
|
settings.githook_content=Contenu du Hook
|
||||||
settings.update_githook=Mettre le Hook à jour
|
settings.update_githook=Mettre le Hook à jour
|
||||||
settings.add_webhook_desc=Nous vous enverrons une demande <code>POST</code> à l'URL ci-dessous avec les détails de tous les événements souscrites. Vous pouvez également spécifier quel format de données vous souhaitez recevoir (JSON, <code>x-www-formulaires-urlencoded</code>, <em>etc.</em>). Plus d'informations se trouvent dans le <a target="_blank" href="%s"> Guide de le Webhooks</a>.
|
settings.add_webhook_desc=Une requête <code>POST</code> sera transmise vers l'URL spécifiée selon l'événement produit. Vous pouvez également choisir le format souhaité pour la réception des données (JSON, x-www-form-urlencoded, XML etc). Pour plus d'infos, lisez le <a target="_blank" href="%s">Guide des WebHooks</a>.
|
||||||
settings.payload_url=URL des Données Utiles
|
settings.payload_url=URL des Données Utiles
|
||||||
settings.content_type=Type de contenu
|
settings.content_type=Type de contenu
|
||||||
settings.secret=Confidentiel
|
settings.secret=Confidentiel
|
||||||
|
@ -589,13 +595,13 @@ settings.event_push_only=Uniquement les <code>push</code> (soumissions).
|
||||||
settings.event_send_everything=J'ai besoin de <strong>tout</strong>.
|
settings.event_send_everything=J'ai besoin de <strong>tout</strong>.
|
||||||
settings.event_choose=Permettez-moi de choisir ce dont j'ai besoin.
|
settings.event_choose=Permettez-moi de choisir ce dont j'ai besoin.
|
||||||
settings.event_create=Créer
|
settings.event_create=Créer
|
||||||
settings.event_create_desc=Branch, ou Tag créé
|
settings.event_create_desc=Branche, ou Tag créé
|
||||||
settings.event_push=Push
|
settings.event_push=Push
|
||||||
settings.event_push_desc=Git push vers un dépôt
|
settings.event_push_desc=Git push vers un dépôt
|
||||||
settings.active=Actif
|
settings.active=Actif
|
||||||
settings.active_helper=Les détails seront délivrés lorsque ce Hook sera déclenché.
|
settings.active_helper=Les détails seront délivrés lorsque ce Hook sera déclenché.
|
||||||
settings.add_hook_success=Nouveau Webhook ajouté.
|
settings.add_hook_success=Nouveau Webhook ajouté.
|
||||||
settings.update_webhook=Mettre le Webhook à jour
|
settings.update_webhook=Mettre à jour le Webhook
|
||||||
settings.update_hook_success=Webhook mis à jour.
|
settings.update_hook_success=Webhook mis à jour.
|
||||||
settings.delete_webhook=Supprimer le Webhook
|
settings.delete_webhook=Supprimer le Webhook
|
||||||
settings.recent_deliveries=Livraisons récentes
|
settings.recent_deliveries=Livraisons récentes
|
||||||
|
@ -633,25 +639,33 @@ release.stable=Stable
|
||||||
release.edit=Éditer
|
release.edit=Éditer
|
||||||
release.ahead=<strong>%d</strong> commissions à %s depuis cette publication
|
release.ahead=<strong>%d</strong> commissions à %s depuis cette publication
|
||||||
release.source_code=Code Source
|
release.source_code=Code Source
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Nom du tag
|
release.tag_name=Nom du tag
|
||||||
release.target=Cible
|
release.target=Cible
|
||||||
release.tag_helper=Choisissez un tag existant ou créez-en un nouveau à publier.
|
release.tag_helper=Choisissez un tag existant ou créez-en un nouveau à publier.
|
||||||
release.release_title=Titre de la publication
|
release.title=Titre
|
||||||
release.content_with_md=Contenu avec <a href="%s">Démarque(s)</a>
|
release.content=Contenu
|
||||||
release.write=Écrire
|
release.write=Écrire
|
||||||
release.preview=Prévisualiser
|
release.preview=Prévisualiser
|
||||||
release.content_placeholder=Rédiger du contenu
|
|
||||||
release.loading=Chargement…
|
release.loading=Chargement…
|
||||||
release.prerelease_desc=Il s'agit d'une version préliminaire
|
release.prerelease_desc=Il s'agit d'une version préliminaire
|
||||||
release.prerelease_helper=Nous soulignerons que cette version est considérée comme non prête pour la production.
|
release.prerelease_helper=Nous soulignerons que cette version est considérée comme non prête pour la production.
|
||||||
|
release.cancel=Annuler
|
||||||
release.publish=Publier
|
release.publish=Publier
|
||||||
release.save_draft=Sauvegarder le Brouillon
|
release.save_draft=Sauvegarder le Brouillon
|
||||||
release.edit_release=Éditer la publication
|
release.edit_release=Éditer la publication
|
||||||
release.tag_name_already_exist=Une publication avec ce nom de tag a déjà existée.
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
|
release.tag_name_already_exist=Une publication avec ce nom de tag existe déjà.
|
||||||
|
release.downloads=Téléchargements
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Nom d'organisation
|
org_name_holder=Nom d'organisation
|
||||||
org_name_helper=Idéalement, un nom d'organisation devrait être court et mémorable.
|
org_full_name_holder=Nom complet de l'organisation
|
||||||
|
org_name_helper=Idéalement, un nom d'organisation devrait être court et facilement mémorisable.
|
||||||
create_org=Créer une organisation
|
create_org=Créer une organisation
|
||||||
repo_updated=Mis à jour
|
repo_updated=Mis à jour
|
||||||
people=Contacts
|
people=Contacts
|
||||||
|
@ -678,7 +692,7 @@ settings.location=Localisation
|
||||||
settings.update_settings=Valider
|
settings.update_settings=Valider
|
||||||
settings.update_setting_success=Paramètres d'organisation modifiés avec succès.
|
settings.update_setting_success=Paramètres d'organisation modifiés avec succès.
|
||||||
settings.change_orgname_prompt=Cette modification affectera comment des liens se rapportent à l'organisation.
|
settings.change_orgname_prompt=Cette modification affectera comment des liens se rapportent à l'organisation.
|
||||||
settings.update_avatar_success=Les paramètres de l'avatar de l'organisation a été mis à jour avec succès.
|
settings.update_avatar_success=Les paramètres de l'avatar de l'organisation ont été mis à jour avec succès.
|
||||||
settings.delete=Supprimer l'organisation
|
settings.delete=Supprimer l'organisation
|
||||||
settings.delete_account=Supprimer cette organisation
|
settings.delete_account=Supprimer cette organisation
|
||||||
settings.delete_prompt=Cela supprimera cette organisation définitivement. Cette opération est <strong>IRRÉVERSIBLE</strong> !
|
settings.delete_prompt=Cela supprimera cette organisation définitivement. Cette opération est <strong>IRRÉVERSIBLE</strong> !
|
||||||
|
@ -697,7 +711,7 @@ members.conceal=Masquer
|
||||||
members.remove=Exclure
|
members.remove=Exclure
|
||||||
members.leave=Quitter
|
members.leave=Quitter
|
||||||
members.invite_desc=Tapez un nom d'utilisateur pour inviter un nouveau membre chez %s :
|
members.invite_desc=Tapez un nom d'utilisateur pour inviter un nouveau membre chez %s :
|
||||||
members.invite_now=Lancer l'invitation
|
members.invite_now=Envoyer une invitation
|
||||||
|
|
||||||
teams.join=Rejoindre
|
teams.join=Rejoindre
|
||||||
teams.leave=Quitter
|
teams.leave=Quitter
|
||||||
|
@ -748,9 +762,11 @@ dashboard.operation_run=Exécuter
|
||||||
dashboard.clean_unbind_oauth=Nettoyer les associations OAuthes
|
dashboard.clean_unbind_oauth=Nettoyer les associations OAuthes
|
||||||
dashboard.clean_unbind_oauth_success=Tous unbind OAuthes ont été supprimés avec succès.
|
dashboard.clean_unbind_oauth_success=Tous unbind OAuthes ont été supprimés avec succès.
|
||||||
dashboard.delete_inactivate_accounts=Supprimer tous les comptes inactifs
|
dashboard.delete_inactivate_accounts=Supprimer tous les comptes inactifs
|
||||||
dashboard.delete_inactivate_accounts_success=Inactivent tous les comptes ont été supprimés avec succès.
|
dashboard.delete_inactivate_accounts_success=Tous les comptes inactifs ont été supprimés avec succès.
|
||||||
dashboard.delete_repo_archives=Supprimer toutes les archives de référentiels
|
dashboard.delete_repo_archives=Supprimer toutes les archives de référentiels
|
||||||
dashboard.delete_repo_archives_success=Toutes les archives de référentiels ont été supprimés avec succès.
|
dashboard.delete_repo_archives_success=Toutes les archives des référentiels ont été supprimées avec succès.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Collecter les déchets des référentiels
|
dashboard.git_gc_repos=Collecter les déchets des référentiels
|
||||||
dashboard.git_gc_repos_success=Tous les dépôts ont effectué la collecte avec succès.
|
dashboard.git_gc_repos_success=Tous les dépôts ont effectué la collecte avec succès.
|
||||||
dashboard.resync_all_sshkeys=Ré-écrire le fichier '.ssh/authorized_keys' (attention : les clés hors-Gogs vont être perdues)
|
dashboard.resync_all_sshkeys=Ré-écrire le fichier '.ssh/authorized_keys' (attention : les clés hors-Gogs vont être perdues)
|
||||||
|
@ -807,7 +823,8 @@ users.edit_account=Modifier le Compte
|
||||||
users.is_activated=Ce compte est activé
|
users.is_activated=Ce compte est activé
|
||||||
users.is_admin=Ce compte possède un niveau d'accès administrateur
|
users.is_admin=Ce compte possède un niveau d'accès administrateur
|
||||||
users.allow_git_hook=Ce compte dispose des autorisations pour créer des crochets de Git
|
users.allow_git_hook=Ce compte dispose des autorisations pour créer des crochets de Git
|
||||||
users.update_profile=Mettre le profil à jour
|
users.allow_import_local=Ce compte dispose des permissions nécessaire à l'import des dépôts locaux
|
||||||
|
users.update_profile=Mettre à jour le profil
|
||||||
users.delete_account=Supprimer ce Compte
|
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_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.still_has_org=Ce compte a toujours membres de l'organisation, vous avez à gauche ou supprimez tout d'abord.
|
||||||
|
@ -839,14 +856,14 @@ auths.host=Hôte
|
||||||
auths.port=Port
|
auths.port=Port
|
||||||
auths.bind_dn=Bind DN
|
auths.bind_dn=Bind DN
|
||||||
auths.bind_password=Bind mot de passe
|
auths.bind_password=Bind mot de passe
|
||||||
auths.bind_password_helper=AVERTISSEMENT : Ce mot de passe est stocké en texte clair. N'utilisez pas le mot de passe d'un compte doté de privilèges élevé.
|
auths.bind_password_helper=Avertissement : Ce mot de passe est stocké en clair. N'utilisez pas le mot de passe d'un compte doté de privilèges élevés.
|
||||||
auths.user_base=Utilisateur Search Base
|
auths.user_base=Utilisateur Search Base
|
||||||
auths.user_dn=Utilisateur DN
|
auths.user_dn=Utilisateur DN
|
||||||
auths.attribute_name=Attribut du prénom
|
auths.attribute_name=Attribut du prénom
|
||||||
auths.attribute_surname=Attribut du nom de famille
|
auths.attribute_surname=Attribut du nom de famille
|
||||||
auths.attribute_mail=Attribut de l'e-mail
|
auths.attribute_mail=Attribut de l'e-mail
|
||||||
auths.filter=Utilisateur Filter
|
auths.filter=Filtre utilisateur
|
||||||
auths.admin_filter=Administrateur Filter
|
auths.admin_filter=Filtre administrateur
|
||||||
auths.ms_ad_sa=Ms Ad SA
|
auths.ms_ad_sa=Ms Ad SA
|
||||||
auths.smtp_auth=Type d'authentification SMTP
|
auths.smtp_auth=Type d'authentification SMTP
|
||||||
auths.smtphost=Hôte SMTP
|
auths.smtphost=Hôte SMTP
|
||||||
|
@ -865,7 +882,7 @@ auths.update_success=Les paramètre d'authentification a été mis à jour avec
|
||||||
auths.update=Mettre à jour les paramètres d'authentifications
|
auths.update=Mettre à jour les paramètres d'authentifications
|
||||||
auths.delete=Supprimer cette authentification
|
auths.delete=Supprimer cette authentification
|
||||||
auths.delete_auth_title=Suppression de l'authentification
|
auths.delete_auth_title=Suppression de l'authentification
|
||||||
auths.delete_auth_desc=Cette autorisation sera supprimée. Continuer?
|
auths.delete_auth_desc=Cette authentification va être supprimée. voulez-vous continuer ?
|
||||||
auths.deletion_success=L'authentification a été supprimée avec succès !
|
auths.deletion_success=L'authentification a été supprimée avec succès !
|
||||||
|
|
||||||
config.server_config=Configuration du Serveur
|
config.server_config=Configuration du Serveur
|
||||||
|
@ -916,7 +933,7 @@ config.oauth_config=Configuration OAuth
|
||||||
config.oauth_enabled=Activé
|
config.oauth_enabled=Activé
|
||||||
config.cache_config=Configuration du Cache
|
config.cache_config=Configuration du Cache
|
||||||
config.cache_adapter=Adaptateur du Cache
|
config.cache_adapter=Adaptateur du Cache
|
||||||
config.cache_interval=Intervals du Cache
|
config.cache_interval=Intervales du Cache
|
||||||
config.cache_conn=Liaison du Cache
|
config.cache_conn=Liaison du Cache
|
||||||
config.session_config=Configuration de Session
|
config.session_config=Configuration de Session
|
||||||
config.session_provider=Fournisseur de Session
|
config.session_provider=Fournisseur de Session
|
||||||
|
@ -954,13 +971,13 @@ notices.delete_success=Note système supprimée avec succès.
|
||||||
[action]
|
[action]
|
||||||
create_repo=a crée le dépôt <a href="%s">%s</a>
|
create_repo=a crée le dépôt <a href="%s">%s</a>
|
||||||
rename_repo=rebaptisé le dépôt de <code>%[1]s</code> à <a href="%[2]s">%[3]s</a>
|
rename_repo=rebaptisé le dépôt de <code>%[1]s</code> à <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=a soumis à <a href="%s/src/%s">%[2]s</a> chez <a href="%[1]s">%[3]s</a>
|
commit_repo=a soumis à <a href="%[1]s/src/%[2]s">%[3]s</a> chez <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`a ouvert un problème <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`a ouvert un problème <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
create_pull_request=`pull request créée le <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`pull request créée le <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`a commenté le problème <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue=`a commenté le problème <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
merge_pull_request=`pull request fusionné le <a href="%s/pulls/%s">%s#%[2]s</a>`
|
merge_pull_request=`pull request fusionné le <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
transfer_repo=a transféré le dépôt <code>%s</code> à <a href="%s">%s</a>
|
transfer_repo=a transféré le dépôt <code>%s</code> à <a href="%s">%s</a>
|
||||||
push_tag=a tagé <a href="%s/src/%s">%[2]s</a> à <a href="%[1]s">%[3]s</a>
|
push_tag=a soumis le tag <a href="%s/src/%s">%[2]s</a> à <a href="%[1]s">%[3]s</a>
|
||||||
compare_2_commits=Comparer ces 2 commissions
|
compare_2_commits=Comparer ces 2 commissions
|
||||||
|
|
||||||
[tool]
|
[tool]
|
||||||
|
|
|
@ -67,8 +67,8 @@ path=Percorso
|
||||||
sqlite_helper=The file path of SQLite3 or TiDB database.
|
sqlite_helper=The file path of SQLite3 or TiDB database.
|
||||||
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
|
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
|
||||||
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
|
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.
|
no_admin_and_disable_registration=Non puoi disabilitare la registrazione senza aver creato un amministratore.
|
||||||
err_empty_admin_password=Admin password cannot be empty.
|
err_empty_admin_password=La password dell'amministratore non puo' essere vuota.
|
||||||
|
|
||||||
general_title=Impostazioni di Base dell'Applicazione
|
general_title=Impostazioni di Base dell'Applicazione
|
||||||
app_name=Nome Applicazione
|
app_name=Nome Applicazione
|
||||||
|
@ -111,7 +111,7 @@ admin_title=Impostazioni Account Amministratore
|
||||||
admin_name=Nome utente
|
admin_name=Nome utente
|
||||||
admin_password=Password
|
admin_password=Password
|
||||||
confirm_password=Conferma Password
|
confirm_password=Conferma Password
|
||||||
admin_email=E-mail
|
admin_email=Admin E-mail
|
||||||
install_gogs=Installare Gogs
|
install_gogs=Installare Gogs
|
||||||
test_git_failed=Fallito il test del comando git: %v
|
test_git_failed=Fallito il test del comando git: %v
|
||||||
sqlite3_not_available=Questa versione non supporta SQLite3, si prega di scaricare la versione binaria ufficiale da %s, NON la versione gobuild.
|
sqlite3_not_available=Questa versione non supporta SQLite3, si prega di scaricare la versione binaria ufficiale da %s, NON la versione gobuild.
|
||||||
|
@ -130,7 +130,7 @@ my_repos=I miei Repository
|
||||||
collaborative_repos=Repository Condivisi
|
collaborative_repos=Repository Condivisi
|
||||||
my_orgs=Le mie Organizzazioni
|
my_orgs=Le mie Organizzazioni
|
||||||
my_mirrors=I miei Mirror
|
my_mirrors=I miei Mirror
|
||||||
view_home=View %s
|
view_home=Vedi %s
|
||||||
|
|
||||||
issues.in_your_repos=Nei tuoi repository
|
issues.in_your_repos=Nei tuoi repository
|
||||||
|
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Password dimenticata
|
||||||
forget_password=Password dimenticata?
|
forget_password=Password dimenticata?
|
||||||
sign_up_now=Bisogno di un account? Iscriviti ora.
|
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.
|
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_to_account=Accedi al tuo account
|
|
||||||
active_your_account=Attiva il tuo 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.
|
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.
|
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.
|
||||||
|
@ -161,10 +160,11 @@ reset_password_helper=Clicca qui per reimpostare la password
|
||||||
password_too_short=La lunghezza della password non può essere meno 6 caratteri.
|
password_too_short=La lunghezza della password non può essere meno 6 caratteri.
|
||||||
|
|
||||||
[mail]
|
[mail]
|
||||||
activate_account=Please activate your account
|
activate_account=Per favore attiva il tuo account
|
||||||
activate_email=Verifica il tuo indirizzo e-mail
|
activate_email=Verifica il tuo indirizzo e-mail
|
||||||
reset_password=Reimposta la tua password
|
reset_password=Reimposta la tua password
|
||||||
register_success=Registrazione completata con successo, Benvenuto
|
register_success=Registrazione completata con successo, Benvenuto
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Sì
|
yes=Sì
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` deve contenere almeno %s caratteri.`
|
||||||
max_size_error=` deve contenere massimo %s caratteri.`
|
max_size_error=` deve contenere massimo %s caratteri.`
|
||||||
email_error=` non è un indirizzo e-mail valido.`
|
email_error=` non è un indirizzo e-mail valido.`
|
||||||
url_error=` non è un URL valido.`
|
url_error=` non è un URL valido.`
|
||||||
|
include_error=` deve contenere la stringa '%s'.`
|
||||||
unknown_error=Errore sconosciuto:
|
unknown_error=Errore sconosciuto:
|
||||||
captcha_incorrect=Il Captcha non corrisponde.
|
captcha_incorrect=Il Captcha non corrisponde.
|
||||||
password_not_match=Le due password non corrispondono.
|
password_not_match=Le due password non corrispondono.
|
||||||
|
@ -298,12 +299,12 @@ add_key_success=New SSH key '%s' has been added successfully!
|
||||||
delete_key=Elimina
|
delete_key=Elimina
|
||||||
ssh_key_deletion=SSH Key Deletion
|
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_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_success=La chiave SSH e' stata cancellata con successo!
|
||||||
add_on=Aggiunto il
|
add_on=Aggiunto il
|
||||||
last_used=Ultimo accesso il
|
last_used=Ultimo accesso il
|
||||||
no_activity=Nessuna attività recente
|
no_activity=Nessuna attività recente
|
||||||
key_state_desc=Hai utilizzato questa chiave negli ultimi 7 giorni
|
key_state_desc=Hai utilizzato questa chiave negli ultimi 7 giorni
|
||||||
token_state_desc=This token is used in last 7 days
|
token_state_desc=Questo token e' satato utilizzato negli ultimi 7 giorni
|
||||||
|
|
||||||
manage_social=Gestisci gli Account Sociali Associati
|
manage_social=Gestisci gli Account Sociali Associati
|
||||||
social_desc=Questa è un elenco degli account sociali associati. Rimuovere qualsiasi account che non si riconosce.
|
social_desc=Questa è un elenco degli account sociali associati. Rimuovere qualsiasi account che non si riconosce.
|
||||||
|
@ -336,6 +337,7 @@ visibility=Visibilità
|
||||||
visiblity_helper=This repository is <span class="ui red text">Private</span>
|
visiblity_helper=This repository is <span class="ui red text">Private</span>
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
||||||
visiblity_fork_helper=(Change of this value will affect all forks)
|
visiblity_fork_helper=(Change of this value will affect all forks)
|
||||||
|
clone_helper=Hai bisogno di aiuto per la clonazione? Visita <a target="_blank" href="%s">Aiuto</a>!
|
||||||
fork_repo=Forka Repository
|
fork_repo=Forka Repository
|
||||||
fork_from=Forka da
|
fork_from=Forka da
|
||||||
fork_visiblity_helper=Non puoi cambiare la visibilità di un repository forkato.
|
fork_visiblity_helper=Non puoi cambiare la visibilità di un repository forkato.
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Initialize this repository with selected files and template
|
||||||
create_repo=Crea Repository
|
create_repo=Crea Repository
|
||||||
default_branch=Ramo (Branch) predefinito
|
default_branch=Ramo (Branch) predefinito
|
||||||
mirror_interval=Intervallo Mirror (in ore)
|
mirror_interval=Intervallo Mirror (in ore)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Il nome repository %s è riservato.
|
form.name_reserved=Il nome repository %s è riservato.
|
||||||
form.name_pattern_not_allowed=La struttura del nome del repository %s non è consentita.
|
form.name_pattern_not_allowed=La struttura del nome del repository %s non è consentita.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=This repository will be a <span class="text blue">mirror</sp
|
||||||
migrate_repo=Migra Repository
|
migrate_repo=Migra Repository
|
||||||
migrate.clone_address=Duplica Indirizzo
|
migrate.clone_address=Duplica Indirizzo
|
||||||
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
|
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
|
||||||
|
migrate.permission_denied=You are not allowed to import local repositories.
|
||||||
migrate.invalid_local_path=Percorso locale non valido, non esiste o non è una cartella.
|
migrate.invalid_local_path=Percorso locale non valido, non esiste o non è una cartella.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=forkato da
|
forked_from=forkato da
|
||||||
fork_from_self=Non puoi forkare il tuo stesso repository!
|
fork_from_self=Non puoi forkare il tuo stesso repository!
|
||||||
copy_link=Copia
|
copy_link=Copia
|
||||||
copy_link_success=Copied!
|
copy_link_success=Copiato!
|
||||||
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
||||||
click_to_copy=Copia negli appunti
|
|
||||||
copied=OK copiato
|
copied=OK copiato
|
||||||
clone_helper=Hai bisogno di aiuto per la clonazione? Visita <a target="_blank" href="%s">Aiuto</a>!
|
|
||||||
unwatch=Non seguire più
|
unwatch=Non seguire più
|
||||||
watch=Segui
|
watch=Segui
|
||||||
unstar=Togli il voto
|
unstar=Togli il voto
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=Crea nuovo repository da riga di comando
|
||||||
push_exist_repo=Push un repo esistente dalla riga di comando
|
push_exist_repo=Push un repo esistente dalla riga di comando
|
||||||
repo_is_empty=This repository is empty, please come back later!
|
repo_is_empty=This repository is empty, please come back later!
|
||||||
|
|
||||||
|
|
||||||
branch=Ramo (Branch)
|
branch=Ramo (Branch)
|
||||||
tree=Albero (Tree)
|
tree=Albero (Tree)
|
||||||
branch_and_tags=Rami (Branch) & Tag
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Rami (Branch)
|
branches=Rami (Branch)
|
||||||
tags=Tag
|
tags=Tag
|
||||||
issues=Problemi
|
issues=Problemi
|
||||||
|
@ -438,7 +442,7 @@ issues.filter_type.all_issues=Tutti i problemi
|
||||||
issues.filter_type.assigned_to_you=Assegnati a te
|
issues.filter_type.assigned_to_you=Assegnati a te
|
||||||
issues.filter_type.created_by_you=Creati da te
|
issues.filter_type.created_by_you=Creati da te
|
||||||
issues.filter_type.mentioning_you=Che ti riguardano
|
issues.filter_type.mentioning_you=Che ti riguardano
|
||||||
issues.filter_sort=Sort
|
issues.filter_sort=Ordina
|
||||||
issues.filter_sort.latest=Newest
|
issues.filter_sort.latest=Newest
|
||||||
issues.filter_sort.oldest=Oldest
|
issues.filter_sort.oldest=Oldest
|
||||||
issues.filter_sort.recentupdate=Recently updated
|
issues.filter_sort.recentupdate=Recently updated
|
||||||
|
@ -454,10 +458,10 @@ issues.closed_title=Closed
|
||||||
issues.num_comments=%d comments
|
issues.num_comments=%d comments
|
||||||
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content=There is no content yet.
|
issues.no_content=There is no content yet.
|
||||||
issues.close_issue=Close
|
issues.close_issue=Chiudi
|
||||||
issues.close_comment_issue=Close and comment
|
issues.close_comment_issue=Comment and close
|
||||||
issues.reopen_issue=Reopen
|
issues.reopen_issue=Reopen
|
||||||
issues.reopen_comment_issue=Reopen and comment
|
issues.reopen_comment_issue=Comment and reopen
|
||||||
issues.create_comment=Commento
|
issues.create_comment=Commento
|
||||||
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
@ -465,7 +469,7 @@ issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%
|
||||||
issues.poster=Poster
|
issues.poster=Poster
|
||||||
issues.admin=Amministratore
|
issues.admin=Amministratore
|
||||||
issues.owner=Proprietario
|
issues.owner=Proprietario
|
||||||
issues.sign_up_for_free=Sign up for free
|
issues.sign_up_for_free=Registrati gratuitamente
|
||||||
issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a>
|
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.edit=Edit
|
||||||
issues.cancel=Cancel
|
issues.cancel=Cancel
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=Elimina Etichetta
|
||||||
issues.label_deletion_desc=Eliminare l'etichetta rimuovera le sue informazioni in tutti i problemi correlati. Vuoi continuare?
|
issues.label_deletion_desc=Eliminare l'etichetta rimuovera le sue informazioni in tutti i problemi correlati. Vuoi continuare?
|
||||||
issues.label_deletion_success=Etichetta eliminata con successo!
|
issues.label_deletion_success=Etichetta eliminata con successo!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Compare Changes
|
pulls.compare_changes=Compare Changes
|
||||||
pulls.compare_changes_desc=Compare two branches and make a pull request for changes.
|
pulls.compare_changes_desc=Compare two branches and make a pull request for changes.
|
||||||
pulls.compare_base=base
|
pulls.compare_base=base
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=Title
|
||||||
milestones.desc=Description
|
milestones.desc=Description
|
||||||
milestones.due_date=Due Date (optional)
|
milestones.due_date=Due Date (optional)
|
||||||
milestones.clear=Clear
|
milestones.clear=Clear
|
||||||
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'.
|
milestones.invalid_due_date_format=Due date format is invalid, must be 'yyyy-mm-dd'.
|
||||||
milestones.create_success=Milestone '%s' has been created successfully!
|
milestones.create_success=Milestone '%s' has been created successfully!
|
||||||
milestones.edit=Edit Milestone
|
milestones.edit=Edit Milestone
|
||||||
milestones.edit_subheader=Use better description for milestones so people won't be confused.
|
milestones.edit_subheader=Use better description for milestones so people won't be confused.
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Conferma eliminazione
|
||||||
settings.add_collaborator=Aggiungi nuovo collaboratore
|
settings.add_collaborator=Aggiungi nuovo collaboratore
|
||||||
settings.add_collaborator_success=Il nuovo collaboratore è stato aggiunto.
|
settings.add_collaborator_success=Il nuovo collaboratore è stato aggiunto.
|
||||||
settings.remove_collaborator_success=Il collaboratore è stato rimosso.
|
settings.remove_collaborator_success=Il collaboratore è stato rimosso.
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=L'utente è un membro dell'organizzazione che non può essere aggiunto come collaboratore.
|
settings.user_is_org_member=L'utente è un membro dell'organizzazione che non può essere aggiunto come collaboratore.
|
||||||
settings.add_webhook=Aggiungi Webhook
|
settings.add_webhook=Aggiungi Webhook
|
||||||
settings.hooks_desc=I Webhooks sono molto simili a un basilare evento trigger HTTP POST. Ogni volta che qualcosa si verifica in Gogs, tratteremo la notifica all'host di destinazione specificato. Ulteriori informazioni in questa <a target="_blank" href="%s">Guida ai Webhooks</a>.
|
settings.hooks_desc=I Webhooks sono molto simili a un basilare evento trigger HTTP POST. Ogni volta che qualcosa si verifica in Gogs, tratteremo la notifica all'host di destinazione specificato. Ulteriori informazioni in questa <a target="_blank" href="%s">Guida ai Webhooks</a>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Stabile
|
||||||
release.edit=modifica
|
release.edit=modifica
|
||||||
release.ahead=<strong>%d</strong> commits da %s da questo rilascio
|
release.ahead=<strong>%d</strong> commits da %s da questo rilascio
|
||||||
release.source_code=Codice Sorgente
|
release.source_code=Codice Sorgente
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Nome tag
|
release.tag_name=Nome tag
|
||||||
release.target=Obbiettivo
|
release.target=Obbiettivo
|
||||||
release.tag_helper=Scegli un tag esistente o crea un nuovo tag una volta pubblicato.
|
release.tag_helper=Scegli un tag esistente o crea un nuovo tag una volta pubblicato.
|
||||||
release.release_title=Nome release
|
release.title=Title
|
||||||
release.content_with_md=Contenuto in <a href="%s">Markdown</a>
|
release.content=Content
|
||||||
release.write=Scrivi
|
release.write=Scrivi
|
||||||
release.preview=Anteprima
|
release.preview=Anteprima
|
||||||
release.content_placeholder=Scrivi qualcosa
|
|
||||||
release.loading=Caricamento...
|
release.loading=Caricamento...
|
||||||
release.prerelease_desc=Questo è un pre-rilascio
|
release.prerelease_desc=Questo è un pre-rilascio
|
||||||
release.prerelease_helper=Precisiamo che questo rilascio non è pronta per la produzione.
|
release.prerelease_helper=Precisiamo che questo rilascio non è pronta per la produzione.
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=Pubblica Rilascio
|
release.publish=Pubblica Rilascio
|
||||||
release.save_draft=Salva Bozza
|
release.save_draft=Salva Bozza
|
||||||
release.edit_release=Modifica Rilascio
|
release.edit_release=Modifica Rilascio
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Un rilascio con questo tag esiste già.
|
release.tag_name_already_exist=Un rilascio con questo tag esiste già.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Nome dell'Organizzazione
|
org_name_holder=Nome dell'Organizzazione
|
||||||
|
org_full_name_holder=Organization Full Name
|
||||||
org_name_helper=Le migliori organizzazioni hanno nomi brevi e memorabili.
|
org_name_helper=Le migliori organizzazioni hanno nomi brevi e memorabili.
|
||||||
create_org=Crea Organizzazione
|
create_org=Crea Organizzazione
|
||||||
repo_updated=Aggiornato
|
repo_updated=Aggiornato
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=Elimina tutti gli account inattivi
|
||||||
dashboard.delete_inactivate_accounts_success=Tutti gli account inattivi eliminati con successo.
|
dashboard.delete_inactivate_accounts_success=Tutti gli account inattivi eliminati con successo.
|
||||||
dashboard.delete_repo_archives=Elimina tutti gli archivi dei repository
|
dashboard.delete_repo_archives=Elimina tutti gli archivi dei repository
|
||||||
dashboard.delete_repo_archives_success=Tutti gli archivi del repository sono stati eliminati con successo.
|
dashboard.delete_repo_archives_success=Tutti gli archivi del repository sono stati eliminati con successo.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Fare la procedura di garbage collection sui repository
|
dashboard.git_gc_repos=Fare la procedura di garbage collection sui repository
|
||||||
dashboard.git_gc_repos_success=Tutti i repository hanno fatto la procedura di garbage collection con successo.
|
dashboard.git_gc_repos_success=Tutti i repository hanno fatto la procedura di garbage collection con successo.
|
||||||
dashboard.resync_all_sshkeys=Riscrivi il file '.ssh/authorized_keys' (attenzione: le chiavi non appartenenti a Gogs saranno perse)
|
dashboard.resync_all_sshkeys=Riscrivi il file '.ssh/authorized_keys' (attenzione: le chiavi non appartenenti a Gogs saranno perse)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Modifica Account
|
||||||
users.is_activated=Questo account è attivato
|
users.is_activated=Questo account è attivato
|
||||||
users.is_admin=Questo account ha permessi di amministratore
|
users.is_admin=Questo account ha permessi di amministratore
|
||||||
users.allow_git_hook=Questo account ha il permesso di creare hooks di Git
|
users.allow_git_hook=Questo account ha il permesso di creare hooks di Git
|
||||||
|
users.allow_import_local=This account has permissions to import local repositories
|
||||||
users.update_profile=Aggiornare Profilo Account
|
users.update_profile=Aggiornare Profilo Account
|
||||||
users.delete_account=Elimina Questo 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_own_repo=Questo account possiede ancora almeno un repository, devi prima cancellarli o trasferirli.
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=Avviso di sistema cancellato con successo.
|
||||||
[action]
|
[action]
|
||||||
create_repo=ha creato il repository <a href="%s">%s</a>
|
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>
|
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>
|
commit_repo=ha pushato nel <a href="%[1]s/src/%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`ha aperto il problema <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`ha aperto il problema <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
create_pull_request=`creata 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>`
|
comment_issue=`ha commentato il problema <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
|
|
|
@ -53,8 +53,8 @@ code=コード
|
||||||
[install]
|
[install]
|
||||||
install=インストール
|
install=インストール
|
||||||
title=初回実行のインストール手順
|
title=初回実行のインストール手順
|
||||||
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!
|
docker_helper=DockerでGogsを稼動する場合、このページに変更を加えるまえに、 <a target="_blank" href="%s">Guidelines</a>をよく読んでください!
|
||||||
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
|
requite_db_desc=Gogs は、MySQL、PostgreSQL、SQLite3 または TiDB が必要です。
|
||||||
db_title=データベース設定
|
db_title=データベース設定
|
||||||
db_type=データベースの種類
|
db_type=データベースの種類
|
||||||
host=ホスト
|
host=ホスト
|
||||||
|
@ -64,11 +64,11 @@ db_name=データベース名
|
||||||
db_helper=Mysql INNODB エンジン utf8_general_ci の文字セットを使用してください。
|
db_helper=Mysql INNODB エンジン utf8_general_ci の文字セットを使用してください。
|
||||||
ssl_mode=SSL モード
|
ssl_mode=SSL モード
|
||||||
path=パス
|
path=パス
|
||||||
sqlite_helper=The file path of SQLite3 or TiDB database.
|
sqlite_helper=SQLite3 または TiDB のデータベースのファイル パス。
|
||||||
err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
|
err_empty_db_path=SQLite3 または TiDB データベースのパスを空にすることはできません。
|
||||||
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
|
err_invalid_tidb_name=TiDB データベース名は文字"."と"-"を許可しない。
|
||||||
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
|
no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
|
||||||
err_empty_admin_password=Admin password cannot be empty.
|
err_empty_admin_password=管理者パスワードは空白にできません。
|
||||||
|
|
||||||
general_title=Gogs の全般設定
|
general_title=Gogs の全般設定
|
||||||
app_name=アプリケーション名
|
app_name=アプリケーション名
|
||||||
|
@ -80,7 +80,7 @@ run_user_helper=ユーザーはリポジトリ ルートパスへのアクセス
|
||||||
domain=ドメイン
|
domain=ドメイン
|
||||||
domain_helper=これはSSHクローンURLに影響する。
|
domain_helper=これはSSHクローンURLに影響する。
|
||||||
ssh_port=SSH ポート
|
ssh_port=SSH ポート
|
||||||
ssh_port_helper=Port number which your SSH server is using, leave it empty to disable SSH feature.
|
ssh_port_helper=あならのSSHサーバおポート番号、SSH機能を無効するにはここを空白のままにしてください。
|
||||||
http_port=HTTP ポート
|
http_port=HTTP ポート
|
||||||
http_port_helper=アプリケーションが待ち受けするポート番号。
|
http_port_helper=アプリケーションが待ち受けするポート番号。
|
||||||
app_url=アプリケーションの URL
|
app_url=アプリケーションの URL
|
||||||
|
@ -103,7 +103,7 @@ disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uplo
|
||||||
disable_registration=自己登録を無効にする
|
disable_registration=自己登録を無効にする
|
||||||
disable_registration_popup=自己登録を無効にし、管理者のみがアカウント作成できる
|
disable_registration_popup=自己登録を無効にし、管理者のみがアカウント作成できる
|
||||||
enable_captcha=Captchaを有効にする
|
enable_captcha=Captchaを有効にする
|
||||||
enable_captcha_popup=Require validate captcha for user self-registration.
|
enable_captcha_popup=ユーザによる自己登録のため、有効なcaptchaが必要です。
|
||||||
require_sign_in_view=サインインしたユーザのみページ閲覧を許可
|
require_sign_in_view=サインインしたユーザのみページ閲覧を許可
|
||||||
require_sign_in_view_popup=サインインしたユーザのみがページを閲覧できます。ビジターはサインインもしくはサインアップページのみ見られます。
|
require_sign_in_view_popup=サインインしたユーザのみがページを閲覧できます。ビジターはサインインもしくはサインアップページのみ見られます。
|
||||||
admin_setting_desc=今管理者アカウントを作成する必要はありません。ID = 1のユーザ は自動的に管理者の権限を獲得します。
|
admin_setting_desc=今管理者アカウントを作成する必要はありません。ID = 1のユーザ は自動的に管理者の権限を獲得します。
|
||||||
|
@ -111,7 +111,7 @@ admin_title=管理者アカウントの設定
|
||||||
admin_name=ユーザ名
|
admin_name=ユーザ名
|
||||||
admin_password=パスワード
|
admin_password=パスワード
|
||||||
confirm_password=パスワード確認
|
confirm_password=パスワード確認
|
||||||
admin_email=E-mail
|
admin_email=管理者の電子メール
|
||||||
install_gogs=Gogs をインストール
|
install_gogs=Gogs をインストール
|
||||||
test_git_failed='Git' コマンドテストに失敗: %v
|
test_git_failed='Git' コマンドテストに失敗: %v
|
||||||
sqlite3_not_available=このリリース バージョンは SQLite3 をサポートしていません。gobuild バージョンではない、公式のバイナリ バージョンを %s からダウンロードしてください。
|
sqlite3_not_available=このリリース バージョンは SQLite3 をサポートしていません。gobuild バージョンではない、公式のバイナリ バージョンを %s からダウンロードしてください。
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=パスワードを忘れた
|
||||||
forget_password=パスワードを忘れた?
|
forget_password=パスワードを忘れた?
|
||||||
sign_up_now=アカウントが必要ですか?今すぐサインアップ
|
sign_up_now=アカウントが必要ですか?今すぐサインアップ
|
||||||
confirmation_mail_sent_prompt=新しい確認メールを <b>%s</b> に送りました。登録を完了させるために、%d時間以内にあなたのメールボックスを確認してください。
|
confirmation_mail_sent_prompt=新しい確認メールを <b>%s</b> に送りました。登録を完了させるために、%d時間以内にあなたのメールボックスを確認してください。
|
||||||
sign_in_to_account=Sign in to your account
|
|
||||||
active_your_account=アカウントをアクティブ
|
active_your_account=アカウントをアクティブ
|
||||||
resent_limit_prompt=申し訳ありませんが、アクティベーションメールは頻繁に送信しています。3 分お待ちください。
|
resent_limit_prompt=申し訳ありませんが、アクティベーションメールは頻繁に送信しています。3 分お待ちください。
|
||||||
has_unconfirmed_mail=こんにちは %s さん、あなたの電子メール アドレス (<b>%s</b>) は未確認です。もし確認メールをまだ確認できていないか、改めて再送信する場合は、下のボタンをクリックしてください。
|
has_unconfirmed_mail=こんにちは %s さん、あなたの電子メール アドレス (<b>%s</b>) は未確認です。もし確認メールをまだ確認できていないか、改めて再送信する場合は、下のボタンをクリックしてください。
|
||||||
|
@ -161,10 +160,11 @@ reset_password_helper=パスワードをリセットするにはここをクリ
|
||||||
password_too_short=6文字未満のパスワードは設定できません。
|
password_too_short=6文字未満のパスワードは設定できません。
|
||||||
|
|
||||||
[mail]
|
[mail]
|
||||||
activate_account=Please activate your account
|
activate_account=あなたのアカウントを有効にしてください。
|
||||||
activate_email=Verify your e-mail address
|
activate_email=電子メール アドレスを確認します。
|
||||||
reset_password=Reset your password
|
reset_password=パスワードをリセットします.
|
||||||
register_success=Register success, Welcome
|
register_success=ようこそ、登録成功
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=はい
|
yes=はい
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=' 少なくとも %s 文字の必要があります '
|
||||||
max_size_error=' %s 文字以下の必要があります '
|
max_size_error=' %s 文字以下の必要があります '
|
||||||
email_error=' は有効な電子メール アドレスではない '
|
email_error=' は有効な電子メール アドレスではない '
|
||||||
url_error=' は有効な URL はありません。 '
|
url_error=' は有効な URL はありません。 '
|
||||||
|
include_error=' 文字列 '%s' を含める必要があります。 '
|
||||||
unknown_error=不明なエラー:
|
unknown_error=不明なエラー:
|
||||||
captcha_incorrect=Captcha が一致しませんでした。
|
captcha_incorrect=Captcha が一致しませんでした。
|
||||||
password_not_match=パスワードと確認用パスワードが一致同していません。
|
password_not_match=パスワードと確認用パスワードが一致同していません。
|
||||||
|
@ -252,7 +253,7 @@ location=ロケーション
|
||||||
update_profile=プロファイル更新
|
update_profile=プロファイル更新
|
||||||
update_profile_success=あなたのプロフィールが更新されました。
|
update_profile_success=あなたのプロフィールが更新されました。
|
||||||
change_username=ユーザー名が変更されました
|
change_username=ユーザー名が変更されました
|
||||||
change_username_prompt=This change will affect the way how links relate to your account.
|
change_username_prompt=この変更はリンクをアカウントに関連付ける方法に影響します。
|
||||||
continue=続行
|
continue=続行
|
||||||
cancel=キャンセル
|
cancel=キャンセル
|
||||||
|
|
||||||
|
@ -267,7 +268,7 @@ update_avatar_success=あなたのアバターの設定が更新されました
|
||||||
change_password=パスワードを変更
|
change_password=パスワードを変更
|
||||||
old_password=現在のパスワード
|
old_password=現在のパスワード
|
||||||
new_password=新しいパスワード
|
new_password=新しいパスワード
|
||||||
retype_new_password=Retype New Password
|
retype_new_password=新しいパスワードを再入力します。
|
||||||
password_incorrect=現在のパスワードが正しくありません。
|
password_incorrect=現在のパスワードが正しくありません。
|
||||||
change_password_success=パスワードが正常に変更されました。今すぐ新しいパスワード経由でサインインすることができます。
|
change_password_success=パスワードが正常に変更されました。今すぐ新しいパスワード経由でサインインすることができます。
|
||||||
|
|
||||||
|
@ -277,9 +278,9 @@ email_desc=あなたのプライマリメールアドレスは、通知やその
|
||||||
primary=プライマリー
|
primary=プライマリー
|
||||||
primary_email=プライマリに設定
|
primary_email=プライマリに設定
|
||||||
delete_email=削除
|
delete_email=削除
|
||||||
email_deletion=E-mail Deletion
|
email_deletion=電子メールの削除
|
||||||
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
|
email_deletion_desc=この電子メール アドレスを削除すると、あなたのアカウントの関連情報も削除されます。続行しますか。
|
||||||
email_deletion_success=E-mail has been deleted successfully!
|
email_deletion_success=電子メールが正常に削除されました。
|
||||||
add_new_email=新しいe-mailアドレスを追加
|
add_new_email=新しいe-mailアドレスを追加
|
||||||
add_email=電子メールを追加します。
|
add_email=電子メールを追加します。
|
||||||
add_email_confirmation_sent='%s' に新しい確認メールを送信しました、次の %d 時間以内に受信トレイを確認し、確認プロセスを完了してください。
|
add_email_confirmation_sent='%s' に新しい確認メールを送信しました、次の %d 時間以内に受信トレイを確認し、確認プロセスを完了してください。
|
||||||
|
@ -334,8 +335,9 @@ repo_name=リポジトリ名
|
||||||
repo_name_helper=偉大なリポジトリ名は短い。思い出に残り、そして<strong>一意</strong>だ。
|
repo_name_helper=偉大なリポジトリ名は短い。思い出に残り、そして<strong>一意</strong>だ。
|
||||||
visibility=ビジビリティ
|
visibility=ビジビリティ
|
||||||
visiblity_helper=このリポジトリは<span class="ui red text">プライベート</span>です。
|
visiblity_helper=このリポジトリは<span class="ui red text">プライベート</span>です。
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=サイト管理者は、強制的にすべての新しいリポジトリを<span class="ui red text"> プライベート</span> にしています。
|
||||||
visiblity_fork_helper=(この値の変更はすべてのフォークに適用されます)
|
visiblity_fork_helper=(この値の変更はすべてのフォークに適用されます)
|
||||||
|
clone_helper=クローニングのヘルプが必要ですか?<a target="_blank"href="%s"> ヘルプ</a> を参照してください!
|
||||||
fork_repo=フォークのリポジトリ
|
fork_repo=フォークのリポジトリ
|
||||||
fork_from=フォーク元
|
fork_from=フォーク元
|
||||||
fork_visiblity_helper=フォークされたリポジトリは可視状態を変更できません
|
fork_visiblity_helper=フォークされたリポジトリは可視状態を変更できません
|
||||||
|
@ -345,31 +347,34 @@ repo_lang_helper=.gitignoreファイルを選択
|
||||||
license=ライセンス
|
license=ライセンス
|
||||||
license_helper=ライセンス ファイルを選択
|
license_helper=ライセンス ファイルを選択
|
||||||
readme=Readme
|
readme=Readme
|
||||||
readme_helper=Select a readme template
|
readme_helper=Readme ファイルのテンプレートを選択
|
||||||
auto_init=Initialize this repository with selected files and template
|
auto_init=選択されたファイルおよびテンプレートでリポジトリを初期化
|
||||||
create_repo=リポジトリを作成
|
create_repo=リポジトリを作成
|
||||||
default_branch=デフォルトのブランチ
|
default_branch=デフォルトのブランチ
|
||||||
mirror_interval=ミラー 間隔(時)
|
mirror_interval=ミラー 間隔(時)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=リポジトリ名 '%s' は予約されています。
|
form.name_reserved=リポジトリ名 '%s' は予約されています。
|
||||||
form.name_pattern_not_allowed=リポジトリ名のパターン '%s' は許可されていません。
|
form.name_pattern_not_allowed=リポジトリ名のパターン '%s' は許可されていません。
|
||||||
|
|
||||||
need_auth=認証が必要
|
need_auth=認証が必要
|
||||||
migrate_type=マイグレーションの種類
|
migrate_type=マイグレーションの種類
|
||||||
migrate_type_helper=This repository will be a <span class="text blue">mirror</span>
|
migrate_type_helper=このリポジトリは、<span class="text blue"> ミラー</span> になります
|
||||||
migrate_repo=リポジトリを移行
|
migrate_repo=リポジトリを移行
|
||||||
migrate.clone_address=クローンアドレス
|
migrate.clone_address=クローンアドレス
|
||||||
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
|
migrate.clone_address_desc=これは、HTTP/HTTPS/GIT URL またはローカル サーバー パスを設定できます。
|
||||||
|
migrate.permission_denied=You are not allowed to import local repositories.
|
||||||
migrate.invalid_local_path=ローカルパスが無効です。存在しないかディレクトリではありません。
|
migrate.invalid_local_path=ローカルパスが無効です。存在しないかディレクトリではありません。
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=フォーク元
|
forked_from=フォーク元
|
||||||
fork_from_self=すでにあなたの所有しているリポジトリはフォークできません
|
fork_from_self=すでにあなたの所有しているリポジトリはフォークできません
|
||||||
copy_link=コピー
|
copy_link=コピー
|
||||||
copy_link_success=Copied!
|
copy_link_success=コピーされました!
|
||||||
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
copy_link_error=⌘ C または Ctrl-C キーを押してコピー
|
||||||
click_to_copy=クリップボードにコピー
|
|
||||||
copied=コピー成功
|
copied=コピー成功
|
||||||
clone_helper=クローニングのヘルプが必要ですか?<a target="_blank"href="%s"> ヘルプ</a> を参照してください!
|
|
||||||
unwatch=Unwatch
|
unwatch=Unwatch
|
||||||
watch=Watch
|
watch=Watch
|
||||||
unstar=Unstar
|
unstar=Unstar
|
||||||
|
@ -381,12 +386,11 @@ quick_guide=クイック ガイド
|
||||||
clone_this_repo=このリポジトリのクローンを作成
|
clone_this_repo=このリポジトリのクローンを作成
|
||||||
create_new_repo_command=コマンドラインで新しいリポジトリを作成します。
|
create_new_repo_command=コマンドラインで新しいリポジトリを作成します。
|
||||||
push_exist_repo=コマンド ・ ラインから既存のリポジトリをプッシュ
|
push_exist_repo=コマンド ・ ラインから既存のリポジトリをプッシュ
|
||||||
repo_is_empty=This repository is empty, please come back later!
|
repo_is_empty=このリポジトリは空です、後で戻って来て下さい!
|
||||||
|
|
||||||
|
|
||||||
branch=ブランチ
|
branch=ブランチ
|
||||||
tree=ツリー
|
tree=ツリー
|
||||||
branch_and_tags=ブランチ& タグ
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=ブランチ
|
branches=ブランチ
|
||||||
tags=タグ
|
tags=タグ
|
||||||
issues=課題
|
issues=課題
|
||||||
|
@ -432,7 +436,7 @@ issues.filter_label_no_select=選択したラベルがありません。
|
||||||
issues.filter_milestone=マイルストーン
|
issues.filter_milestone=マイルストーン
|
||||||
issues.filter_milestone_no_select=選択されたマイルストーンなし
|
issues.filter_milestone_no_select=選択されたマイルストーンなし
|
||||||
issues.filter_assignee=アサインされた人
|
issues.filter_assignee=アサインされた人
|
||||||
issues.filter_assginee_no_select=No selected Assignee
|
issues.filter_assginee_no_select=選択可能な担当者がいない
|
||||||
issues.filter_type=タイプ
|
issues.filter_type=タイプ
|
||||||
issues.filter_type.all_issues=すべての問題
|
issues.filter_type.all_issues=すべての問題
|
||||||
issues.filter_type.assigned_to_you=あなたに割り当てられました。
|
issues.filter_type.assigned_to_you=あなたに割り当てられました。
|
||||||
|
@ -441,35 +445,35 @@ issues.filter_type.mentioning_you=あなたに伝える
|
||||||
issues.filter_sort=並べ替え
|
issues.filter_sort=並べ替え
|
||||||
issues.filter_sort.latest=最新
|
issues.filter_sort.latest=最新
|
||||||
issues.filter_sort.oldest=最も古い
|
issues.filter_sort.oldest=最も古い
|
||||||
issues.filter_sort.recentupdate=Recently updated
|
issues.filter_sort.recentupdate=最近更新された
|
||||||
issues.filter_sort.leastupdate=Least recently updated
|
issues.filter_sort.leastupdate=Least recently updated
|
||||||
issues.filter_sort.mostcomment=Most commented
|
issues.filter_sort.mostcomment=一番多いコメント
|
||||||
issues.filter_sort.leastcomment=Least commented
|
issues.filter_sort.leastcomment=一番少ないコメント
|
||||||
issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a>
|
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.opened_by_fake=opened %[1]s by %[2]s
|
||||||
issues.previous=前ページ
|
issues.previous=前ページ
|
||||||
issues.next=次ページ
|
issues.next=次ページ
|
||||||
issues.open_title=Open
|
issues.open_title=オープン
|
||||||
issues.closed_title=Closed
|
issues.closed_title=クローズ
|
||||||
issues.num_comments=%d comments
|
issues.num_comments=%d コメント
|
||||||
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content=There is no content yet.
|
issues.no_content=まだコンテンツがありません
|
||||||
issues.close_issue=Close
|
issues.close_issue=閉じる
|
||||||
issues.close_comment_issue=Close and comment
|
issues.close_comment_issue=コメントと閉じる
|
||||||
issues.reopen_issue=Reopen
|
issues.reopen_issue=Reopen
|
||||||
issues.reopen_comment_issue=Reopen and comment
|
issues.reopen_comment_issue=コメントと再開
|
||||||
issues.create_comment=Comment
|
issues.create_comment=コメント
|
||||||
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.poster=Poster
|
issues.poster=ポスター
|
||||||
issues.admin=Admin
|
issues.admin=アドミン
|
||||||
issues.owner=Owner
|
issues.owner=オーナー
|
||||||
issues.sign_up_for_free=Sign up for free
|
issues.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.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a>
|
||||||
issues.edit=Edit
|
issues.edit=編集
|
||||||
issues.cancel=Cancel
|
issues.cancel=キャンセル
|
||||||
issues.save=Save
|
issues.save=保存
|
||||||
issues.label_title=ラベル名
|
issues.label_title=ラベル名
|
||||||
issues.label_color=ラベルの色
|
issues.label_color=ラベルの色
|
||||||
issues.label_count=%d ラベル
|
issues.label_count=%d ラベル
|
||||||
|
@ -481,29 +485,30 @@ issues.label_deletion=ラベルの削除
|
||||||
issues.label_deletion_desc=ラベルを削除すると、関連するすべての問題の情報が削除されます。続行しますか。
|
issues.label_deletion_desc=ラベルを削除すると、関連するすべての問題の情報が削除されます。続行しますか。
|
||||||
issues.label_deletion_success=ラベルは正常に削除されました。
|
issues.label_deletion_success=ラベルは正常に削除されました。
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=変更を比較
|
pulls.compare_changes=変更を比較
|
||||||
pulls.compare_changes_desc=2つのブランチを比較し、プルリクエストを作成します。
|
pulls.compare_changes_desc=2つのブランチを比較し、プルリクエストを作成します。
|
||||||
pulls.compare_base=base
|
pulls.compare_base=ベース
|
||||||
pulls.compare_compare=compare
|
pulls.compare_compare=比較
|
||||||
pulls.filter_branch=Filter branch
|
pulls.filter_branch=フィルターブランチ
|
||||||
pulls.no_results=結果が見つかりませんでした。
|
pulls.no_results=結果が見つかりませんでした。
|
||||||
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even.
|
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.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=プルリクエストを作成します。
|
||||||
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code>
|
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.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
|
||||||
pulls.tab_conversation=Conversation
|
pulls.tab_conversation=会話
|
||||||
pulls.tab_commits=Commits
|
pulls.tab_commits=コミット
|
||||||
pulls.tab_files=Files changed
|
pulls.tab_files=ファイルが変更された
|
||||||
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
|
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
|
||||||
pulls.merged=Merged
|
pulls.merged=マージされた
|
||||||
pulls.has_merged=This pull request has been merged successfully!
|
pulls.has_merged=このプルプルリクエストは正常にマージされました!
|
||||||
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
|
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
|
||||||
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
|
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
|
||||||
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
|
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_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.cannot_auto_merge_helper=それを解決するためにコマンド ライン ツールを使用してください。
|
||||||
pulls.merge_pull_request=Merge Pull Request
|
pulls.merge_pull_request=プルリクエストをマージします。
|
||||||
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
|
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
|
||||||
|
|
||||||
milestones.new=新しいマイルストーン
|
milestones.new=新しいマイルストーン
|
||||||
|
@ -513,22 +518,22 @@ milestones.closed=%s を閉じました
|
||||||
milestones.no_due_date=期限なし
|
milestones.no_due_date=期限なし
|
||||||
milestones.open=開く
|
milestones.open=開く
|
||||||
milestones.close=閉じる
|
milestones.close=閉じる
|
||||||
milestones.new_subheader=Create milestones to organize your issues.
|
milestones.new_subheader=あなたの課題を整理するためマイルス トーンを作成します。
|
||||||
milestones.create=Create Milestone
|
milestones.create=マイルス トーンを作成
|
||||||
milestones.title=Title
|
milestones.title=タイトル
|
||||||
milestones.desc=Description
|
milestones.desc=説明
|
||||||
milestones.due_date=Due Date (optional)
|
milestones.due_date=期日 (オプション)
|
||||||
milestones.clear=Clear
|
milestones.clear=消去
|
||||||
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'.
|
milestones.invalid_due_date_format=期限日付のフォーマットが無効、'yyyy-mm-dd' のフォーマットが必要です。
|
||||||
milestones.create_success=Milestone '%s' has been created successfully!
|
milestones.create_success=マイルス トーン '%s' が正常に作成されました!
|
||||||
milestones.edit=Edit Milestone
|
milestones.edit=マイルス トーンを編集
|
||||||
milestones.edit_subheader=Use better description for milestones so people won't be confused.
|
milestones.edit_subheader=人々を混乱させないため、マイルス トーンにより良い説明を使用します。
|
||||||
milestones.cancel=Cancel
|
milestones.cancel=キャンセル
|
||||||
milestones.modify=Modify Milestone
|
milestones.modify=マイルス トーンを変更します。
|
||||||
milestones.edit_success=Changes of milestone '%s' has been saved successfully!
|
milestones.edit_success=マイルス トーン '%s' の変更が正常に保存されました。
|
||||||
milestones.deletion=Milestone Deletion
|
milestones.deletion=マイルス トーンの削除
|
||||||
milestones.deletion_desc=Delete this milestone will remove its information in all related issues. Do you want to continue?
|
milestones.deletion_desc=このマイルス トーンを削除すると、関連課題に該当情報が削除されます。続行しますか。
|
||||||
milestones.deletion_success=Milestone has been deleted successfully!
|
milestones.deletion_success=マイルス トーンは正常に削除されました。
|
||||||
|
|
||||||
settings=設定
|
settings=設定
|
||||||
settings.options=オプション
|
settings.options=オプション
|
||||||
|
@ -539,15 +544,15 @@ settings.basic_settings=基本設定
|
||||||
settings.danger_zone=危険地帯
|
settings.danger_zone=危険地帯
|
||||||
settings.site=公式サイト
|
settings.site=公式サイト
|
||||||
settings.update_settings=設定の更新
|
settings.update_settings=設定の更新
|
||||||
settings.change_reponame_prompt=This change will affect how links relate to the repository.
|
settings.change_reponame_prompt=この変更はリンクがリポジトリに関連付ける方法に影響します。
|
||||||
settings.transfer=オーナー移転
|
settings.transfer=オーナー移転
|
||||||
settings.transfer_desc=リポジトリをあなたが管理者権限を持っている別のユーザーまた組織に移譲します。
|
settings.transfer_desc=リポジトリをあなたが管理者権限を持っている別のユーザーまた組織に移譲します。
|
||||||
settings.new_owner_has_same_repo=新しいオーナーは、既に同じ名前のリポジトリを持っています。
|
settings.new_owner_has_same_repo=新しいオーナーは、既に同じ名前のリポジトリを持っています。
|
||||||
settings.delete=このリポジトリを削除
|
settings.delete=このリポジトリを削除
|
||||||
settings.delete_desc=リポジトリを削除すると元に戻せません。確実に確認してください。
|
settings.delete_desc=リポジトリを削除すると元に戻せません。確実に確認してください。
|
||||||
settings.transfer_notices_1=- You will lose access if new owner is a individual user.
|
settings.transfer_notices_1=-新しい所有者が個人ユーザーの場合、あなたがアクセスできなくなります。
|
||||||
settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners.
|
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.transfer_form_title=操作を確認するために、以下の情報を入力してください。
|
||||||
settings.delete_notices_1=- This operation <strong>CANNOT</strong> be undone.
|
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.delete_notices_2=- This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators.
|
||||||
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion.
|
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion.
|
||||||
|
@ -561,17 +566,18 @@ settings.confirm_delete=削除の確認
|
||||||
settings.add_collaborator=新しい共同編集者を追加
|
settings.add_collaborator=新しい共同編集者を追加
|
||||||
settings.add_collaborator_success=新しい共同編集者が追加されました。
|
settings.add_collaborator_success=新しい共同編集者が追加されました。
|
||||||
settings.remove_collaborator_success=共同編集者が削除されました。
|
settings.remove_collaborator_success=共同編集者が削除されました。
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=ユーザーは組織の一員なので、共同編集者として追加することはできません。
|
settings.user_is_org_member=ユーザーは組織の一員なので、共同編集者として追加することはできません。
|
||||||
settings.add_webhook=Webhook を追加
|
settings.add_webhook=Webhook を追加
|
||||||
settings.hooks_desc=Webhooksは、Gogsで特定のイベントの発生時に指定された外部サービスに通知を許可します。イベントが発生すると、それぞれ指定されたUrlに、POSTリクエストが送られます。詳細はこちらのの <a target="_blank"href="%s"> Webhooks ガイド</a>をご覧ください。
|
settings.hooks_desc=Webhooksは、Gogsで特定のイベントの発生時に指定された外部サービスに通知を許可します。イベントが発生すると、それぞれ指定されたUrlに、POSTリクエストが送られます。詳細はこちらのの <a target="_blank"href="%s"> Webhooks ガイド</a>をご覧ください。
|
||||||
settings.webhook_deletion=Delete Webhook
|
settings.webhook_deletion=Webhook を削除
|
||||||
settings.webhook_deletion_desc=Delete this webhook will remove its information and all delivery history. Do you want to continue?
|
settings.webhook_deletion_desc=このwebhookを削除すると、すべての情報と配信履歴が削除されます。続行しますか?
|
||||||
settings.webhook_deletion_success=Webhook has been deleted successfully!
|
settings.webhook_deletion_success=Webhook が正常に削除されました。
|
||||||
settings.webhook.request=Request
|
settings.webhook.request=リクエスト
|
||||||
settings.webhook.response=Response
|
settings.webhook.response=レスポンス
|
||||||
settings.webhook.headers=Headers
|
settings.webhook.headers=ヘッダ
|
||||||
settings.webhook.payload=Payload
|
settings.webhook.payload=ペイロード
|
||||||
settings.webhook.body=Body
|
settings.webhook.body=ボディ
|
||||||
settings.githooks_desc=Git のフックは Git 自体によって提供されています。以下のリストのファイルを編集して、サポートされているフックのカスタム操作を適用することができます。
|
settings.githooks_desc=Git のフックは Git 自体によって提供されています。以下のリストのファイルを編集して、サポートされているフックのカスタム操作を適用することができます。
|
||||||
settings.githook_edit_desc=もしフックがアクティブではない場合は、サンプルコンテンツが表示されます。コンテンツを空白にするにはこのフックを無効にします。
|
settings.githook_edit_desc=もしフックがアクティブではない場合は、サンプルコンテンツが表示されます。コンテンツを空白にするにはこのフックを無効にします。
|
||||||
settings.githook_name=フックの名前
|
settings.githook_name=フックの名前
|
||||||
|
@ -581,17 +587,17 @@ settings.add_webhook_desc=私たちは、指定されたURLに購読されたイ
|
||||||
settings.payload_url=ペイロードの URL
|
settings.payload_url=ペイロードの URL
|
||||||
settings.content_type=コンテンツ タイプ
|
settings.content_type=コンテンツ タイプ
|
||||||
settings.secret=秘密
|
settings.secret=秘密
|
||||||
settings.slack_username=Username
|
settings.slack_username=ユーザ名
|
||||||
settings.slack_icon_url=Icon URL
|
settings.slack_icon_url=アイコン URL
|
||||||
settings.slack_color=Color
|
settings.slack_color=カラー
|
||||||
settings.event_desc=どのイベントをこのWEBフックのトリガーにしますか?
|
settings.event_desc=どのイベントをこのWEBフックのトリガーにしますか?
|
||||||
settings.event_push_only=<code>push</code> イベントのみ
|
settings.event_push_only=<code>push</code> イベントのみ
|
||||||
settings.event_send_everything=I need <strong>everything</strong>.
|
settings.event_send_everything=<strong>すべて</strong> が必要です。
|
||||||
settings.event_choose=Let me choose what I need.
|
settings.event_choose=必要なものを選択しましょう。
|
||||||
settings.event_create=Create
|
settings.event_create=Create
|
||||||
settings.event_create_desc=Branch, or tag created
|
settings.event_create_desc=ブランチ、またはタグを作成
|
||||||
settings.event_push=Push
|
settings.event_push=プッシュ
|
||||||
settings.event_push_desc=Git push to a repository
|
settings.event_push_desc=Git リポジトリにプッシュ
|
||||||
settings.active=アクティブ
|
settings.active=アクティブ
|
||||||
settings.active_helper=このフックのトリガーが引かれた時に、イベントの詳細を配信します。
|
settings.active_helper=このフックのトリガーが引かれた時に、イベントの詳細を配信します。
|
||||||
settings.add_hook_success=新しい webhook が追加されました。
|
settings.add_hook_success=新しい webhook が追加されました。
|
||||||
|
@ -605,16 +611,16 @@ settings.slack_token=トークン
|
||||||
settings.slack_domain=ドメイン
|
settings.slack_domain=ドメイン
|
||||||
settings.slack_channel=チャンネル
|
settings.slack_channel=チャンネル
|
||||||
settings.deploy_keys=デプロイキー
|
settings.deploy_keys=デプロイキー
|
||||||
settings.add_deploy_key=Add Deploy Key
|
settings.add_deploy_key=デプロイキーを追加
|
||||||
settings.no_deploy_keys=You haven't added any deploy key.
|
settings.no_deploy_keys=でプロキーは1つも追加されていません。
|
||||||
settings.title=Title
|
settings.title=タイトル
|
||||||
settings.deploy_key_content=Content
|
settings.deploy_key_content=コンテント
|
||||||
settings.key_been_used=Deploy key content has been used.
|
settings.key_been_used=デプロイキーが使用されています。
|
||||||
settings.key_name_used=Deploy key with same name has already existed.
|
settings.key_name_used=同じ名前のデプロイキーが既に存在しています。
|
||||||
settings.add_key_success=New deploy key '%s' has been added successfully!
|
settings.add_key_success=新しいデプロイキー '%s'が正常に追加されました!
|
||||||
settings.deploy_key_deletion=Delete Deploy Key
|
settings.deploy_key_deletion=デプロイキーを削除
|
||||||
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_desc=このデプロイキーを削除すると、このリポジトリに関連するすべてのアクセス権も削除されます。続行しますか。
|
||||||
settings.deploy_key_deletion_success=Deploy key has been deleted successfully!
|
settings.deploy_key_deletion_success=デプロイキーが正常に削除された!
|
||||||
|
|
||||||
diff.browse_source=ソースを参照
|
diff.browse_source=ソースを参照
|
||||||
diff.parent=親
|
diff.parent=親
|
||||||
|
@ -633,24 +639,32 @@ release.stable=安定
|
||||||
release.edit=編集
|
release.edit=編集
|
||||||
release.ahead=このリリース以降 %s へ <strong>%d</strong> コミット
|
release.ahead=このリリース以降 %s へ <strong>%d</strong> コミット
|
||||||
release.source_code=ソース コード
|
release.source_code=ソース コード
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=タグ名
|
release.tag_name=タグ名
|
||||||
release.target=ターゲット
|
release.target=ターゲット
|
||||||
release.tag_helper=既存のタグを選択するか、新しいタグを作成し発行します。
|
release.tag_helper=既存のタグを選択するか、新しいタグを作成し発行します。
|
||||||
release.release_title=リリース タイトル
|
release.title=Title
|
||||||
release.content_with_md=<a href="%s"> Markdown</a> コンテンツ
|
release.content=Content
|
||||||
release.write=書込み
|
release.write=書込み
|
||||||
release.preview=プレビュー
|
release.preview=プレビュー
|
||||||
release.content_placeholder=コンテンツを書く
|
|
||||||
release.loading=読み込み中…
|
release.loading=読み込み中…
|
||||||
release.prerelease_desc=これはリリース前のものです
|
release.prerelease_desc=これはリリース前のものです
|
||||||
release.prerelease_helper=このリリースは非プロダクション利用として識別します。
|
release.prerelease_helper=このリリースは非プロダクション利用として識別します。
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=リリースを発行
|
release.publish=リリースを発行
|
||||||
release.save_draft=下書きを保存
|
release.save_draft=下書きを保存
|
||||||
release.edit_release=リリースを編集
|
release.edit_release=リリースを編集
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=このタグ名には既にリリースが存在します。
|
release.tag_name_already_exist=このタグ名には既にリリースが存在します。
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=組織名
|
org_name_holder=組織名
|
||||||
|
org_full_name_holder=組織のフルネーム
|
||||||
org_name_helper=偉大な組織の名は短く覚えやすいです。
|
org_name_helper=偉大な組織の名は短く覚えやすいです。
|
||||||
create_org=組織を作成
|
create_org=組織を作成
|
||||||
repo_updated=更新した
|
repo_updated=更新した
|
||||||
|
@ -736,7 +750,7 @@ notices=システム通知
|
||||||
monitor=モニタリング
|
monitor=モニタリング
|
||||||
first_page=First
|
first_page=First
|
||||||
last_page=Last
|
last_page=Last
|
||||||
total=Total: %d
|
total=合計: %d
|
||||||
|
|
||||||
dashboard.statistic=統計
|
dashboard.statistic=統計
|
||||||
dashboard.operations=操作
|
dashboard.operations=操作
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=非アクティブのアカウントをす
|
||||||
dashboard.delete_inactivate_accounts_success=すべての非アクティブアカウントは正常に削除されました。
|
dashboard.delete_inactivate_accounts_success=すべての非アクティブアカウントは正常に削除されました。
|
||||||
dashboard.delete_repo_archives=リポジトリのすべてのアーカイブを削除
|
dashboard.delete_repo_archives=リポジトリのすべてのアーカイブを削除
|
||||||
dashboard.delete_repo_archives_success=リポジトリのすべてのアーカイブが正常に削除されました。
|
dashboard.delete_repo_archives_success=リポジトリのすべてのアーカイブが正常に削除されました。
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=リポジトリでのガベージコレクションを実行します。
|
dashboard.git_gc_repos=リポジトリでのガベージコレクションを実行します。
|
||||||
dashboard.git_gc_repos_success=すべてのリポジトリは正常にガベージ コレクションを行いました。
|
dashboard.git_gc_repos_success=すべてのリポジトリは正常にガベージ コレクションを行いました。
|
||||||
dashboard.resync_all_sshkeys='.ssh/ authorized_keys' ファイルを再生成します。(警告:Gogsキー以外は失われます)
|
dashboard.resync_all_sshkeys='.ssh/ authorized_keys' ファイルを再生成します。(警告:Gogsキー以外は失われます)
|
||||||
|
@ -795,23 +811,24 @@ users.activated=アクティブ化
|
||||||
users.admin=アドミン
|
users.admin=アドミン
|
||||||
users.repos=リポジトリ
|
users.repos=リポジトリ
|
||||||
users.created=作成されました
|
users.created=作成されました
|
||||||
users.send_register_notify=Send Registration Notification To User
|
users.send_register_notify=登録通知をユーザーに送信
|
||||||
users.new_success=New account '%s' has been created successfully.
|
users.new_success=New account '%s' has been created successfully.
|
||||||
users.edit=編集
|
users.edit=編集
|
||||||
users.auth_source=Authentication Source
|
users.auth_source=認証ソース
|
||||||
users.local=ローカル
|
users.local=ローカル
|
||||||
users.auth_login_name=Authentication Login Name
|
users.auth_login_name=認証ログイン名
|
||||||
users.password_helper=Leave it empty to remain unchanged.
|
users.password_helper=それをそのまま空のままにします。
|
||||||
users.update_profile_success=アカウントのプロファイルが更新されました。
|
users.update_profile_success=アカウントのプロファイルが更新されました。
|
||||||
users.edit_account=アカウントの編集
|
users.edit_account=アカウントの編集
|
||||||
users.is_activated=アカウントがアクティブされました
|
users.is_activated=アカウントがアクティブされました
|
||||||
users.is_admin=このアカウントには管理者の権限を持つ
|
users.is_admin=このアカウントには管理者の権限を持つ
|
||||||
users.allow_git_hook=このアカウントには Git のフックを作成する権限を持つ
|
users.allow_git_hook=このアカウントには Git のフックを作成する権限を持つ
|
||||||
|
users.allow_import_local=This account has permissions to import local repositories
|
||||||
users.update_profile=アカウント ・ プロファイルを更新
|
users.update_profile=アカウント ・ プロファイルを更新
|
||||||
users.delete_account=このアカウントを削除
|
users.delete_account=このアカウントを削除
|
||||||
users.still_own_repo=アカウント所有のリポジトリがあり、リポジトリの削除または所有者の移譲が必要です。
|
users.still_own_repo=アカウント所有のリポジトリがあり、リポジトリの削除または所有者の移譲が必要です。
|
||||||
users.still_has_org=アカウントはまだ組織のメンバーであり、組織から退出するか削除する必要があります。
|
users.still_has_org=アカウントはまだ組織のメンバーであり、組織から退出するか削除する必要があります。
|
||||||
users.deletion_success=Account has been deleted successfully!
|
users.deletion_success=アカウントが正常に削除されました。
|
||||||
|
|
||||||
orgs.org_manage_panel=組織の管理パネル
|
orgs.org_manage_panel=組織の管理パネル
|
||||||
orgs.name=名前
|
orgs.name=名前
|
||||||
|
@ -826,47 +843,47 @@ repos.watches=Watches
|
||||||
repos.stars=Stars
|
repos.stars=Stars
|
||||||
repos.issues=課題
|
repos.issues=課題
|
||||||
|
|
||||||
auths.auth_manage_panel=Authentication Manage Panel
|
auths.auth_manage_panel=認証管理パネル
|
||||||
auths.new=Add New Source
|
auths.new=新しいソースを追加
|
||||||
auths.name=名前
|
auths.name=名前
|
||||||
auths.type=タイプ
|
auths.type=タイプ
|
||||||
auths.enabled=Enabled
|
auths.enabled=Enabled
|
||||||
auths.updated=Updated
|
auths.updated=Updated
|
||||||
auths.auth_type=Authentication Type
|
auths.auth_type=認証タイプ
|
||||||
auths.auth_name=Authentication Name
|
auths.auth_name=認証名
|
||||||
auths.domain=ドメイン
|
auths.domain=ドメイン
|
||||||
auths.host=ホスト
|
auths.host=ホスト
|
||||||
auths.port=ポート
|
auths.port=ポート
|
||||||
auths.bind_dn=Bind DN
|
auths.bind_dn=バインド DN
|
||||||
auths.bind_password=Bind Password
|
auths.bind_password=バインド パスワード
|
||||||
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
|
auths.bind_password_helper=警告: このパスワードは暗号化されずに格納されます。特権を持つアカウントに使用しないでください。
|
||||||
auths.user_base=User Search Base
|
auths.user_base=ユーザ検索ベース
|
||||||
auths.user_dn=User DN
|
auths.user_dn=User DN
|
||||||
auths.attribute_name=名前属性
|
auths.attribute_name=名前属性
|
||||||
auths.attribute_surname=名字属性
|
auths.attribute_surname=名字属性
|
||||||
auths.attribute_mail=Eメール属性
|
auths.attribute_mail=Eメール属性
|
||||||
auths.filter=User Filter
|
auths.filter=User フィルター
|
||||||
auths.admin_filter=Admin Filter
|
auths.admin_filter=Admin フィルター
|
||||||
auths.ms_ad_sa=Ms Ad SA
|
auths.ms_ad_sa=Ms Ad SA
|
||||||
auths.smtp_auth=SMTP Authentication Type
|
auths.smtp_auth=SMTP 認証の種類
|
||||||
auths.smtphost=SMTP ホスト
|
auths.smtphost=SMTP ホスト
|
||||||
auths.smtpport=SMTP ポート
|
auths.smtpport=SMTP ポート
|
||||||
auths.allowed_domains=Allowed Domains
|
auths.allowed_domains=許可されているドメイン
|
||||||
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
|
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
|
||||||
auths.enable_tls=TLS 暗号化を有効にする
|
auths.enable_tls=TLS 暗号化を有効にする
|
||||||
auths.skip_tls_verify=Skip TLS Verify
|
auths.skip_tls_verify=TLSベリファイを省略
|
||||||
auths.pam_service_name=PAMサービス名
|
auths.pam_service_name=PAMサービス名
|
||||||
auths.enable_auto_register=自動登録を有効にする
|
auths.enable_auto_register=自動登録を有効にする
|
||||||
auths.tips=ヒント
|
auths.tips=ヒント
|
||||||
auths.edit=Edit Authentication Setting
|
auths.edit=認証設定を編集
|
||||||
auths.activated=認証がアクティブされました
|
auths.activated=認証がアクティブされました
|
||||||
auths.new_success=New authentication '%s' has been added successfully.
|
auths.new_success=新しい認証 '%s' が正常に追加されました。
|
||||||
auths.update_success=Authentication setting has been updated successfully.
|
auths.update_success=認証の設定が正常に更新されました。
|
||||||
auths.update=Update Authentication Setting
|
auths.update=認証設定を更新
|
||||||
auths.delete=Delete This Authentication
|
auths.delete=この認証を削除
|
||||||
auths.delete_auth_title=Authentication Deletion
|
auths.delete_auth_title=認証削除
|
||||||
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
|
auths.delete_auth_desc=認証を削除します、継続しますか?
|
||||||
auths.deletion_success=Authentication has been deleted successfully!
|
auths.deletion_success=認証が正常に削除されました。
|
||||||
|
|
||||||
config.server_config=サーバーの構成
|
config.server_config=サーバーの構成
|
||||||
config.app_name=アプリケーション名
|
config.app_name=アプリケーション名
|
||||||
|
@ -898,12 +915,12 @@ config.show_registration_button=登録ボタンを表示します。
|
||||||
config.require_sign_in_view=サインインを要求
|
config.require_sign_in_view=サインインを要求
|
||||||
config.enable_cache_avatar=アバターのキャッシュを有効にします。
|
config.enable_cache_avatar=アバターのキャッシュを有効にします。
|
||||||
config.mail_notify=メール通知
|
config.mail_notify=メール通知
|
||||||
config.disable_key_size_check=Disable Minimum Key Size Check
|
config.disable_key_size_check=最小キー サイズ チェックを無効にします
|
||||||
config.enable_captcha=Enable Captcha
|
config.enable_captcha=Captchaを有効にする
|
||||||
config.active_code_lives=コードリンクの有効期限をアクティブ
|
config.active_code_lives=コードリンクの有効期限をアクティブ
|
||||||
config.reset_password_code_lives=パスワードリンクの有効期限をリセット
|
config.reset_password_code_lives=パスワードリンクの有効期限をリセット
|
||||||
config.webhook_config=Webhook設定
|
config.webhook_config=Webhook設定
|
||||||
config.queue_length=Queue Length
|
config.queue_length=キューの長さ
|
||||||
config.deliver_timeout=送信タイムアウト
|
config.deliver_timeout=送信タイムアウト
|
||||||
config.skip_tls_verify=TLSの確認を省略
|
config.skip_tls_verify=TLSの確認を省略
|
||||||
config.mailer_config=メーラーの構成
|
config.mailer_config=メーラーの構成
|
||||||
|
@ -953,12 +970,12 @@ notices.delete_success=システム通知が正常に削除されました。
|
||||||
|
|
||||||
[action]
|
[action]
|
||||||
create_repo=リポジトリ <a href="%s"> %s</a>を作成しました
|
create_repo=リポジトリ <a href="%s"> %s</a>を作成しました
|
||||||
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
rename_repo=<code>%[1]s</code> から <a href="%[2]s">[3]s</a> にリポジトリ名を変更した
|
||||||
commit_repo=<a href="%[1]s">%[3]s</a>を<a href="%[1]s/src/%[2]s">%[2]s</a>にプッシュしました
|
commit_repo=<a href="%[1]s">%[4]s</a>を<a href="%[1]s/src/%[2]s">%[3]s</a>にプッシュしました
|
||||||
create_issue=`問題 <a href="%s/issues/%s">%s#%[2]s</a> を開きました`
|
create_issue=`問題 <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=`プルリクエスト <a href="%s/pulls/%s"> %s[2]s</a>を作成`
|
||||||
comment_issue=`問題 <a href="%s/issues/%s">%s#%[2]s</a> のコメント`
|
comment_issue=`問題 <a href="%s/issues/%s">%s#%[2]s</a> のコメント`
|
||||||
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
merge_pull_request=`プルリクエスト <a href="%s/pulls/%s"> %s[2]s</a>をマージしました`
|
||||||
transfer_repo=リポジトリ <code>%s</code> を <a href="%s">%s</a> へ転送しました
|
transfer_repo=リポジトリ <code>%s</code> を <a href="%s">%s</a> へ転送しました
|
||||||
push_tag=<a href="%[1]s">%[3]s</a> に タグ <a href="%[1]s/src/%[2]s">%[2]s</a> をプッシュしました
|
push_tag=<a href="%[1]s">%[3]s</a> に タグ <a href="%[1]s/src/%[2]s">%[2]s</a> をプッシュしました
|
||||||
compare_2_commits=これら 2 のコミットの比較を閲覧する
|
compare_2_commits=これら 2 のコミットの比較を閲覧する
|
||||||
|
@ -985,8 +1002,8 @@ raw_seconds=秒
|
||||||
raw_minutes=分
|
raw_minutes=分
|
||||||
|
|
||||||
[dropzone]
|
[dropzone]
|
||||||
default_message=Drop files here or click to upload.
|
default_message=ここにファイルをドロップまたはクリックしてアップロードします。
|
||||||
invalid_input_type=You can't upload files of this type.
|
invalid_input_type=このタイプのファイルはアップロードできません.
|
||||||
file_too_big=File size({{filesize}} MB) exceeds maximum size({{maxFilesize}} MB).
|
file_too_big=ファイルのサイズ ({{filesize}} MB) では、最大サイズ ({{maxFilesize}} MB) を超えています。
|
||||||
remove_file=Remove file
|
remove_file=ファイル削除
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ admin_title=Admin konta iestatījumi
|
||||||
admin_name=Lietotājvārds
|
admin_name=Lietotājvārds
|
||||||
admin_password=Parole
|
admin_password=Parole
|
||||||
confirm_password=Apstipriniet paroli
|
confirm_password=Apstipriniet paroli
|
||||||
admin_email=E-pasts
|
admin_email=Administratora e-pasts
|
||||||
install_gogs=Instalēt Gogs
|
install_gogs=Instalēt Gogs
|
||||||
test_git_failed=Kļūda pārbaudot 'git' komandu: %v
|
test_git_failed=Kļūda pārbaudot 'git' komandu: %v
|
||||||
sqlite3_not_available=Jūsu versija neatbalsta SQLite3, lūdzu lejupielādējiet oficiālo bināro versiju no %s, NEVIS gobuild versiju.
|
sqlite3_not_available=Jūsu versija neatbalsta SQLite3, lūdzu lejupielādējiet oficiālo bināro versiju no %s, NEVIS gobuild versiju.
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Aizmirsu paroli
|
||||||
forget_password=Aizmirsi paroli?
|
forget_password=Aizmirsi paroli?
|
||||||
sign_up_now=Nepieciešams konts? Reģistrējies tagad.
|
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.
|
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_to_account=Pierakstīties savā kontā
|
|
||||||
active_your_account=Aktivizēt savu kontu
|
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.
|
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.
|
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.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Lūdzu, aktivizējiet savu kontu
|
||||||
activate_email=Apstipriniet savu e-pasta adresi
|
activate_email=Apstipriniet savu e-pasta adresi
|
||||||
reset_password=Atiestatīt savu paroli
|
reset_password=Atiestatīt savu paroli
|
||||||
register_success=Reģistrācija notikusi veiksmīgi
|
register_success=Reģistrācija notikusi veiksmīgi
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Jā
|
yes=Jā
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` jabūt vismaz %s simbolu garumā.`
|
||||||
max_size_error=` jabūt ne mazāk kā %s simbolu garumā.`
|
max_size_error=` jabūt ne mazāk kā %s simbolu garumā.`
|
||||||
email_error=` nav derīga e-pasta adrese.`
|
email_error=` nav derīga e-pasta adrese.`
|
||||||
url_error=` nav korekts URL.`
|
url_error=` nav korekts URL.`
|
||||||
|
include_error=` ir jāsatur tekstu '%s'.`
|
||||||
unknown_error=Nezināma kļūda:
|
unknown_error=Nezināma kļūda:
|
||||||
captcha_incorrect=Pārbaudes kods nesakrīt ar attēlā redzamo.
|
captcha_incorrect=Pārbaudes kods nesakrīt ar attēlā redzamo.
|
||||||
password_not_match=Parole un atkārtoti ievadītā parole nav vienādas.
|
password_not_match=Parole un atkārtoti ievadītā parole nav vienādas.
|
||||||
|
@ -336,6 +337,7 @@ visibility=Redzamība
|
||||||
visiblity_helper=Šis repozitorijs ir <span class="ui red text">privāts</span>
|
visiblity_helper=Šis repozitorijs ir <span class="ui red text">privāts</span>
|
||||||
visiblity_helper_forced=Lapas administrators ir noteicis, ka visiem repozitorijiem ir jābūt <span class="ui red text">privātiem</span>
|
visiblity_helper_forced=Lapas administrators ir noteicis, ka visiem repozitorijiem ir jābūt <span class="ui red text">privātiem</span>
|
||||||
visiblity_fork_helper=(Šīs vērtības maiņa ietekmēs arī visus atdalītos repozitorijus)
|
visiblity_fork_helper=(Šīs vērtības maiņa ietekmēs arī visus atdalītos repozitorijus)
|
||||||
|
clone_helper=Nepieciešama palīdzība kā veikt klonēšana? Apmeklējiet <a target="_blank" href="%s">Palīdzība</a> lapu!
|
||||||
fork_repo=Atdalīt repozitoriju
|
fork_repo=Atdalīt repozitoriju
|
||||||
fork_from=Atdalīt no
|
fork_from=Atdalīt no
|
||||||
fork_visiblity_helper=Atdalītam repozitorijam nav iespējams nomainīt tā redzamību
|
fork_visiblity_helper=Atdalītam repozitorijam nav iespējams nomainīt tā redzamību
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Inicializēt šo repozitoriju ar izvēlētajiem failiem un sagatavi
|
||||||
create_repo=Izveidot repozitoriju
|
create_repo=Izveidot repozitoriju
|
||||||
default_branch=Noklusējuma atzars
|
default_branch=Noklusējuma atzars
|
||||||
mirror_interval=Spoguļošanas intervāls (stundās)
|
mirror_interval=Spoguļošanas intervāls (stundās)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Repozitorija nosaukums '%s' ir rezervēts.
|
form.name_reserved=Repozitorija nosaukums '%s' ir rezervēts.
|
||||||
form.name_pattern_not_allowed=Repozitorija nosaukums '%s' nav atļauts.
|
form.name_pattern_not_allowed=Repozitorija nosaukums '%s' nav atļauts.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=Šis repozitorijs būs <span class="text blue">spogulis</spa
|
||||||
migrate_repo=Migrēt repozitoriju
|
migrate_repo=Migrēt repozitoriju
|
||||||
migrate.clone_address=Klonēšanas adrese
|
migrate.clone_address=Klonēšanas adrese
|
||||||
migrate.clone_address_desc=Tas var būt HTTP/HTTPS/GIT URL vai ceļš uz lokālā servera.
|
migrate.clone_address_desc=Tas var būt HTTP/HTTPS/GIT URL vai ceļš uz lokālā servera.
|
||||||
|
migrate.permission_denied=Jums nav tiesību importēt lokālu repozitoriju.
|
||||||
migrate.invalid_local_path=Nekorents lokālais ceļš, tas neeksistē vai nav direktorijs.
|
migrate.invalid_local_path=Nekorents lokālais ceļš, tas neeksistē vai nav direktorijs.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=atdalīts no
|
forked_from=atdalīts no
|
||||||
fork_from_self=Nav iespējams atdalīt repozitoriju, kuram esat īpašnieks!
|
fork_from_self=Nav iespējams atdalīt repozitoriju, kuram esat īpašnieks!
|
||||||
copy_link=Kopēt
|
copy_link=Kopēt
|
||||||
copy_link_success=Nokopēts!
|
copy_link_success=Nokopēts!
|
||||||
copy_link_error=Nospiediet ⌘-C vai Ctrl-C, lai nokopētu
|
copy_link_error=Nospiediet ⌘-C vai Ctrl-C, lai nokopētu
|
||||||
click_to_copy=Kopēt uz starpliktuvi
|
|
||||||
copied=Kopēšana notikusi veiksmīgi
|
copied=Kopēšana notikusi veiksmīgi
|
||||||
clone_helper=Nepieciešama palīdzība kā veikt klonēšana? Apmeklējiet <a target="_blank" href="%s">Palīdzība</a> lapu!
|
|
||||||
unwatch=Nevērot
|
unwatch=Nevērot
|
||||||
watch=Vērot
|
watch=Vērot
|
||||||
unstar=Noņemt zvaigznīti
|
unstar=Noņemt zvaigznīti
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=Izveidot jaunu repozitoriju komandrindā
|
||||||
push_exist_repo=Nosūtīt izmaiņas no komandrindas eksistējošam repozitorijam
|
push_exist_repo=Nosūtīt izmaiņas no komandrindas eksistējošam repozitorijam
|
||||||
repo_is_empty=Šis repozitorijs ir tukšs, apskatiet atkal vēlāk!
|
repo_is_empty=Šis repozitorijs ir tukšs, apskatiet atkal vēlāk!
|
||||||
|
|
||||||
|
|
||||||
branch=Atzars
|
branch=Atzars
|
||||||
tree=Koks
|
tree=Koks
|
||||||
branch_and_tags=Atzari un tagi
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Atzari
|
branches=Atzari
|
||||||
tags=Tagi
|
tags=Tagi
|
||||||
issues=Problēmas
|
issues=Problēmas
|
||||||
|
@ -457,7 +461,7 @@ issues.no_content=Vēl nav satura.
|
||||||
issues.close_issue=Aizvērt
|
issues.close_issue=Aizvērt
|
||||||
issues.close_comment_issue=Komentēt un aizvērt
|
issues.close_comment_issue=Komentēt un aizvērt
|
||||||
issues.reopen_issue=Atvērt atkārtoti
|
issues.reopen_issue=Atvērt atkārtoti
|
||||||
issues.reopen_comment_issue=Atvērt atkārtoti un komentēt
|
issues.reopen_comment_issue=Komentēt un atvērt atkārtoti
|
||||||
issues.create_comment=Komentēt
|
issues.create_comment=Komentēt
|
||||||
issues.closed_at=`aizvērts <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at=`aizvērts <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at=`atvērts atkārtoti <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at=`atvērts atkārtoti <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=Etiķetes dzēšana
|
||||||
issues.label_deletion_desc=Dzēšot šo etiķeti, tā tiks noņemta no visām saistītajām problēmām. Vai vēlaties turpināt?
|
issues.label_deletion_desc=Dzēšot šo etiķeti, tā tiks noņemta no visām saistītajām problēmām. Vai vēlaties turpināt?
|
||||||
issues.label_deletion_success=Etiķete tika veiksmīgi izdzēsta!
|
issues.label_deletion_success=Etiķete tika veiksmīgi izdzēsta!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Salīdzināt izmaiņas
|
pulls.compare_changes=Salīdzināt izmaiņas
|
||||||
pulls.compare_changes_desc=Salīdzināt divus atzarus un izveidot izmaiņu pieprasījumu.
|
pulls.compare_changes_desc=Salīdzināt divus atzarus un izveidot izmaiņu pieprasījumu.
|
||||||
pulls.compare_base=pamata
|
pulls.compare_base=pamata
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Apstiprināt dzēšanu
|
||||||
settings.add_collaborator=Pievienot jaunu līdzstrādnieku
|
settings.add_collaborator=Pievienot jaunu līdzstrādnieku
|
||||||
settings.add_collaborator_success=Jauns līdzstrādnieks ir pievienots.
|
settings.add_collaborator_success=Jauns līdzstrādnieks ir pievienots.
|
||||||
settings.remove_collaborator_success=Līdzstrādnieks tika noņemts.
|
settings.remove_collaborator_success=Līdzstrādnieks tika noņemts.
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=Lietotājs ir organizācijas biedrs, kas nevar tikt pievienots kā līdzstrādnieks.
|
settings.user_is_org_member=Lietotājs ir organizācijas biedrs, kas nevar tikt pievienots kā līdzstrādnieks.
|
||||||
settings.add_webhook=Pievienot tīmekļa āķi
|
settings.add_webhook=Pievienot tīmekļa āķi
|
||||||
settings.hooks_desc=Tīmekļa āķi ļauj paziņot ārējiem servisiem par noteiktiem notikomiem, kas notiek Git servisā. Kad iestāsies kāds notikums, katram ārējā servisa URL tiks nosūtīts POST pieprasījums. Lai uzzinātu sīkāk skatieties <a target="_blank" href="%s">Tīmekļa āķu rokasgrāmatā</a>.
|
settings.hooks_desc=Tīmekļa āķi ļauj paziņot ārējiem servisiem par noteiktiem notikomiem, kas notiek Git servisā. Kad iestāsies kāds notikums, katram ārējā servisa URL tiks nosūtīts POST pieprasījums. Lai uzzinātu sīkāk skatieties <a target="_blank" href="%s">Tīmekļa āķu rokasgrāmatā</a>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Stabila
|
||||||
release.edit=labot
|
release.edit=labot
|
||||||
release.ahead=<strong>%d</strong> revīzijas atzarā %s kopš šī laidiena
|
release.ahead=<strong>%d</strong> revīzijas atzarā %s kopš šī laidiena
|
||||||
release.source_code=Izejas kods
|
release.source_code=Izejas kods
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Taga nosaukums
|
release.tag_name=Taga nosaukums
|
||||||
release.target=Mērķis
|
release.target=Mērķis
|
||||||
release.tag_helper=Publicējot, izvēlieties esošu vai izveidojiet jaunu tagu.
|
release.tag_helper=Publicējot, izvēlieties esošu vai izveidojiet jaunu tagu.
|
||||||
release.release_title=Laidiena virsraksts
|
release.title=Title
|
||||||
release.content_with_md=Saturs ar <a href="%s">Markdown</a>
|
release.content=Content
|
||||||
release.write=Rakstīt
|
release.write=Rakstīt
|
||||||
release.preview=Priekšskatītījums
|
release.preview=Priekšskatītījums
|
||||||
release.content_placeholder=Uzrakstiet kādu aprakstu
|
|
||||||
release.loading=Notiek ielāde...
|
release.loading=Notiek ielāde...
|
||||||
release.prerelease_desc=Šī ir pirmslaidiena versija
|
release.prerelease_desc=Šī ir pirmslaidiena versija
|
||||||
release.prerelease_helper=Tiks norādīts, ka šis laidiens nav gatavs lietošanai produkcijas režīmā.
|
release.prerelease_helper=Tiks norādīts, ka šis laidiens nav gatavs lietošanai produkcijas režīmā.
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=Publicēt laidienu
|
release.publish=Publicēt laidienu
|
||||||
release.save_draft=Saglabāt melnrakstu
|
release.save_draft=Saglabāt melnrakstu
|
||||||
release.edit_release=Labot laidienu
|
release.edit_release=Labot laidienu
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Laidiens ar šādu taga nosaukumu jau eksistē.
|
release.tag_name_already_exist=Laidiens ar šādu taga nosaukumu jau eksistē.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Organizācijas nosaukums
|
org_name_holder=Organizācijas nosaukums
|
||||||
|
org_full_name_holder=Organizācijas pilnais nosaukums
|
||||||
org_name_helper=Labi organizāciju nosaukumi ir īsi un tādi, kurus viegli atcerēties.
|
org_name_helper=Labi organizāciju nosaukumi ir īsi un tādi, kurus viegli atcerēties.
|
||||||
create_org=Izveidot organizāciju
|
create_org=Izveidot organizāciju
|
||||||
repo_updated=Atjaunināts
|
repo_updated=Atjaunināts
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=Dzēst visus neaktīvos kontus
|
||||||
dashboard.delete_inactivate_accounts_success=Visi neaktīvie kotni tika veiksmīgi izdzēsti.
|
dashboard.delete_inactivate_accounts_success=Visi neaktīvie kotni tika veiksmīgi izdzēsti.
|
||||||
dashboard.delete_repo_archives=Dzēst visu repozitoriju arhīvus
|
dashboard.delete_repo_archives=Dzēst visu repozitoriju arhīvus
|
||||||
dashboard.delete_repo_archives_success=Visu repozitoriju arhīvi tika veiksmīgi izdzēsti.
|
dashboard.delete_repo_archives_success=Visu repozitoriju arhīvi tika veiksmīgi izdzēsti.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Veikt repozitoriju datu sakārtošānu (git gc)
|
dashboard.git_gc_repos=Veikt repozitoriju datu sakārtošānu (git gc)
|
||||||
dashboard.git_gc_repos_success=Datu sakārtošana visiem repozitorijiem veiksmīgi pabeigta.
|
dashboard.git_gc_repos_success=Datu sakārtošana visiem repozitorijiem veiksmīgi pabeigta.
|
||||||
dashboard.resync_all_sshkeys=Pārrakstīt '.ssh/authorized_keys' failu (brīdinājums: ne-Git atslēgas tiks pazaudētas)
|
dashboard.resync_all_sshkeys=Pārrakstīt '.ssh/authorized_keys' failu (brīdinājums: ne-Git atslēgas tiks pazaudētas)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Labot kontu
|
||||||
users.is_activated=Konts ir aktivizēts
|
users.is_activated=Konts ir aktivizēts
|
||||||
users.is_admin=Šim kontam ir administratora piekļuves tiesības
|
users.is_admin=Šim kontam ir administratora piekļuves tiesības
|
||||||
users.allow_git_hook=Šim kontam ir tiesības pievienot/labot Git āķus
|
users.allow_git_hook=Šim kontam ir tiesības pievienot/labot Git āķus
|
||||||
|
users.allow_import_local=Šim kontam ir tiesības importēt lokālus repozitorijus
|
||||||
users.update_profile=Mainīt konta profilu
|
users.update_profile=Mainīt konta profilu
|
||||||
users.delete_account=Dzēst šo kontu
|
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_own_repo=Šis konts ir vismaz viena repozitorija īpašnieks, tos sākumā ir nepieciešams izdzēst vai nomainīt to īpašnieku.
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=Sistēmas paziņojums tika veiksmīgi izdzēsts.
|
||||||
[action]
|
[action]
|
||||||
create_repo=izveidoja repozitoriju <a href="%s">%s</a>
|
create_repo=izveidoja repozitoriju <a href="%s">%s</a>
|
||||||
rename_repo=pārsauca repozitoriju no <code>%[1]s</code> uz <a href="%[2]s">%[3]s</a>
|
rename_repo=pārsauca repozitoriju no <code>%[1]s</code> uz <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=veica izmaiņu nosūtīšanu atzaram <a href="%s/src/%s">%[2]s</a> repozitorijā <a href="%[1]s">%[3]s</a>
|
commit_repo=veica izmaiņu nosūtīšanu atzaram <a href="%[1]s/src/%[2]s">%[3]s</a> repozitorijā <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`reģistrēja problēmu <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`reģistrēja problēmu <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
create_pull_request=`izveidoja izmaiņu pieprasījumu <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`izveidoja izmaiņu pieprasījumu <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`pievienoja komentāru problēmai <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue=`pievienoja komentāru problēmai <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
|
|
|
@ -111,7 +111,7 @@ admin_title=Instellingen beheerdersaccount
|
||||||
admin_name=Gebruikersnaam
|
admin_name=Gebruikersnaam
|
||||||
admin_password=Wachtwoord
|
admin_password=Wachtwoord
|
||||||
confirm_password=Verifieer wachtwoord
|
confirm_password=Verifieer wachtwoord
|
||||||
admin_email=E-mailadres
|
admin_email=Admin E-mail
|
||||||
install_gogs=Installeer Gogs
|
install_gogs=Installeer Gogs
|
||||||
test_git_failed=Git test niet gelukt: 'git' commando %v
|
test_git_failed=Git test niet gelukt: 'git' commando %v
|
||||||
sqlite3_not_available=Uw versie biedt geen ondersteuning voor SQLite3, download de officiële binaire versie van %s, niet de gobuild versie.
|
sqlite3_not_available=Uw versie biedt geen ondersteuning voor SQLite3, download de officiële binaire versie van %s, niet de gobuild versie.
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Wachtwoord vergeten
|
||||||
forget_password=Wachtwoord vergeten?
|
forget_password=Wachtwoord vergeten?
|
||||||
sign_up_now=Een account nodig? Meld u nu aan.
|
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.
|
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_to_account=Sign in to your account
|
|
||||||
active_your_account=Activeer uw 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.
|
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.
|
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.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Please activate your account
|
||||||
activate_email=Verify your e-mail address
|
activate_email=Verify your e-mail address
|
||||||
reset_password=Reset your password
|
reset_password=Reset your password
|
||||||
register_success=Register success, Welcome
|
register_success=Register success, Welcome
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Ja
|
yes=Ja
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=moet minimaal %s karakters bevatten.
|
||||||
max_size_error=mag maximaal %s karakters bevatten.
|
max_size_error=mag maximaal %s karakters bevatten.
|
||||||
email_error=is niet een valide e-mail adres.
|
email_error=is niet een valide e-mail adres.
|
||||||
url_error=is niet een valide URL.
|
url_error=is niet een valide URL.
|
||||||
|
include_error=` must contain substring '%s'.`
|
||||||
unknown_error=Onbekende fout:
|
unknown_error=Onbekende fout:
|
||||||
captcha_incorrect=Captcha komt niet overeen.
|
captcha_incorrect=Captcha komt niet overeen.
|
||||||
password_not_match=Wachtwoord en verificatie wachtwoord komen niet overeen.
|
password_not_match=Wachtwoord en verificatie wachtwoord komen niet overeen.
|
||||||
|
@ -336,6 +337,7 @@ visibility=Zichtbaarheid
|
||||||
visiblity_helper=Deze repositorie is <span class="ui red text">privaat</span>
|
visiblity_helper=Deze repositorie is <span class="ui red text">privaat</span>
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
||||||
visiblity_fork_helper=(Verandering van deze waarde zal van invloed zijn op alle forks)
|
visiblity_fork_helper=(Verandering van deze waarde zal van invloed zijn op alle forks)
|
||||||
|
clone_helper=De behoeftehulp van klonen? Bezoek <a target="_blank" href="%s"> helpen</a>!
|
||||||
fork_repo=Vork Repository
|
fork_repo=Vork Repository
|
||||||
fork_from=Afsplitsing van
|
fork_from=Afsplitsing van
|
||||||
fork_visiblity_helper=Gevorkte repository wijzigen zijn bereik potentiële kopers niet
|
fork_visiblity_helper=Gevorkte repository wijzigen zijn bereik potentiële kopers niet
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Initialiseer deze repositorie met de geselecteerde bestanden en sjablo
|
||||||
create_repo=Nieuwe Repositorie
|
create_repo=Nieuwe Repositorie
|
||||||
default_branch=Standaard branch
|
default_branch=Standaard branch
|
||||||
mirror_interval=Mirror interval(uur)
|
mirror_interval=Mirror interval(uur)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Repositorienaam '%s' is gereserveerd.
|
form.name_reserved=Repositorienaam '%s' is gereserveerd.
|
||||||
form.name_pattern_not_allowed=Repositorie naampatroon '%s' is niet toegestaan.
|
form.name_pattern_not_allowed=Repositorie naampatroon '%s' is niet toegestaan.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=Deze repositorie zal een <span class="text blue">mirror</spa
|
||||||
migrate_repo=Migreer repositorie
|
migrate_repo=Migreer repositorie
|
||||||
migrate.clone_address=Clone adres
|
migrate.clone_address=Clone adres
|
||||||
migrate.clone_address_desc=Dit kan een HTTP/HTTPS/GIT URL zijn of een lokaal pad.
|
migrate.clone_address_desc=Dit kan een HTTP/HTTPS/GIT URL zijn of een lokaal pad.
|
||||||
|
migrate.permission_denied=You are not allowed to import local repositories.
|
||||||
migrate.invalid_local_path=Ongeldig lokaal pad, het pad bestaat niet of het is geen map.
|
migrate.invalid_local_path=Ongeldig lokaal pad, het pad bestaat niet of het is geen map.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=geforked van
|
forked_from=geforked van
|
||||||
fork_from_self=U kunt geen repository forken die u al beheert!
|
fork_from_self=U kunt geen repository forken die u al beheert!
|
||||||
copy_link=Kopieer
|
copy_link=Kopieer
|
||||||
copy_link_success=Copied!
|
copy_link_success=Copied!
|
||||||
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
||||||
click_to_copy=Kopieer link naar plakbord
|
|
||||||
copied=Gekopieerd
|
copied=Gekopieerd
|
||||||
clone_helper=De behoeftehulp van klonen? Bezoek <a target="_blank" href="%s"> helpen</a>!
|
|
||||||
unwatch=Negeren
|
unwatch=Negeren
|
||||||
watch=Volgen
|
watch=Volgen
|
||||||
unstar=Ontster
|
unstar=Ontster
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=Maak een nieuwe repositorie aan vanaf de console
|
||||||
push_exist_repo=Push een bestaande repositorie vanaf de console
|
push_exist_repo=Push een bestaande repositorie vanaf de console
|
||||||
repo_is_empty=This repository is empty, please come back later!
|
repo_is_empty=This repository is empty, please come back later!
|
||||||
|
|
||||||
|
|
||||||
branch=Aftakking
|
branch=Aftakking
|
||||||
tree=Boom
|
tree=Boom
|
||||||
branch_and_tags=Aftakkingen & labels
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Aftakkingen
|
branches=Aftakkingen
|
||||||
tags=Labels
|
tags=Labels
|
||||||
issues=Kwesties
|
issues=Kwesties
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=Verwijder label
|
||||||
issues.label_deletion_desc=Het verwijderen van dit label zal alle informatie in de gerelateerde problemen verwijderen. Wilt u doorgaan?
|
issues.label_deletion_desc=Het verwijderen van dit label zal alle informatie in de gerelateerde problemen verwijderen. Wilt u doorgaan?
|
||||||
issues.label_deletion_success=Label werd met succes verwijderd!
|
issues.label_deletion_success=Label werd met succes verwijderd!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Vergelijk veranderingen
|
pulls.compare_changes=Vergelijk veranderingen
|
||||||
pulls.compare_changes_desc=Vergelijk twee vertakkingen en maak een pull verzoek voor wijzigingen.
|
pulls.compare_changes_desc=Vergelijk twee vertakkingen en maak een pull verzoek voor wijzigingen.
|
||||||
pulls.compare_base=base
|
pulls.compare_base=base
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=Titel
|
||||||
milestones.desc=Beschrijving
|
milestones.desc=Beschrijving
|
||||||
milestones.due_date=Vervaldatum (optioneel)
|
milestones.due_date=Vervaldatum (optioneel)
|
||||||
milestones.clear=Leegmaken
|
milestones.clear=Leegmaken
|
||||||
milestones.invalid_due_date_format=Formaat vervaldatum is ongeldig, moet zijn "jaar-mm-dd".
|
milestones.invalid_due_date_format=Formaat vervaldatum is ongeldig, moet zijn "jjjj-mm-dd".
|
||||||
milestones.create_success=Mijlpaal '%s' is met succes aangemaakt!
|
milestones.create_success=Mijlpaal '%s' is met succes aangemaakt!
|
||||||
milestones.edit=Bewerk mijlpaal
|
milestones.edit=Bewerk mijlpaal
|
||||||
milestones.edit_subheader=Gebruik een goede beschrijving voor mijlpalen, om verwarring te voorkomen.
|
milestones.edit_subheader=Gebruik een goede beschrijving voor mijlpalen, om verwarring te voorkomen.
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Bevestig verwijdering
|
||||||
settings.add_collaborator=Nieuwe medewerker toevoegen
|
settings.add_collaborator=Nieuwe medewerker toevoegen
|
||||||
settings.add_collaborator_success=medewerker is toegevoegd.
|
settings.add_collaborator_success=medewerker is toegevoegd.
|
||||||
settings.remove_collaborator_success=medewerker is verwijderd.
|
settings.remove_collaborator_success=medewerker is verwijderd.
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=Gebruiker is lid van de organisatie die als een medewerker kan niet worden toegevoegd.
|
settings.user_is_org_member=Gebruiker is lid van de organisatie die als een medewerker kan niet worden toegevoegd.
|
||||||
settings.add_webhook=Webhook toevoegen
|
settings.add_webhook=Webhook toevoegen
|
||||||
settings.hooks_desc=Webhooks dat de externe diensten om kennisgevingen te ontvangen wanneer bepaalde gebeurtenissen op Gogs plaatsvinden. Wanneer de opgegeven gebeurtenissen plaatsvinden, sturen we een POST-aanvraag naar elk van de URL's die u opgeeft. Meer informatie vindt u in onze <a target="_blank" href="%s"> Webhooks gids</a>.
|
settings.hooks_desc=Webhooks dat de externe diensten om kennisgevingen te ontvangen wanneer bepaalde gebeurtenissen op Gogs plaatsvinden. Wanneer de opgegeven gebeurtenissen plaatsvinden, sturen we een POST-aanvraag naar elk van de URL's die u opgeeft. Meer informatie vindt u in onze <a target="_blank" href="%s"> Webhooks gids</a>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Stabiel
|
||||||
release.edit=bewerken
|
release.edit=bewerken
|
||||||
release.ahead=<strong>%d</strong> aanpassingen aan %s sinds deze versie
|
release.ahead=<strong>%d</strong> aanpassingen aan %s sinds deze versie
|
||||||
release.source_code=Broncode
|
release.source_code=Broncode
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Tagnaam
|
release.tag_name=Tagnaam
|
||||||
release.target=Doel
|
release.target=Doel
|
||||||
release.tag_helper=Kies een bestaande tag, of creëer een nieuwe tag bij publiceren.
|
release.tag_helper=Kies een bestaande tag, of creëer een nieuwe tag bij publiceren.
|
||||||
release.release_title=Release titel
|
release.title=Title
|
||||||
release.content_with_md=Inhoud met <a href="%s">Markeringen</a>
|
release.content=Content
|
||||||
release.write=Schrijf
|
release.write=Schrijf
|
||||||
release.preview=Voorbeeld
|
release.preview=Voorbeeld
|
||||||
release.content_placeholder=Schrijf enige inhoud
|
|
||||||
release.loading=Laden...
|
release.loading=Laden...
|
||||||
release.prerelease_desc=Dit is een beta-versie
|
release.prerelease_desc=Dit is een beta-versie
|
||||||
release.prerelease_helper=Wij wijzen u erop dat deze release is niet geschikt voor productie doeleinden.
|
release.prerelease_helper=Wij wijzen u erop dat deze release is niet geschikt voor productie doeleinden.
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=Release publiceren
|
release.publish=Release publiceren
|
||||||
release.save_draft=Concept opslaan
|
release.save_draft=Concept opslaan
|
||||||
release.edit_release=Release bewerken
|
release.edit_release=Release bewerken
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Versie met deze naam bestaat al.
|
release.tag_name_already_exist=Versie met deze naam bestaat al.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Organisatienaam
|
org_name_holder=Organisatienaam
|
||||||
|
org_full_name_holder=Organization Full Name
|
||||||
org_name_helper=Een goede organisatienaam is kort en memorabel.
|
org_name_helper=Een goede organisatienaam is kort en memorabel.
|
||||||
create_org=Nieuwe organisatie aanmaken
|
create_org=Nieuwe organisatie aanmaken
|
||||||
repo_updated=Geupdate
|
repo_updated=Geupdate
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=Verwijder alle inactieve accounts
|
||||||
dashboard.delete_inactivate_accounts_success=Alle inactivering van rekeningen hebben verwijderd.
|
dashboard.delete_inactivate_accounts_success=Alle inactivering van rekeningen hebben verwijderd.
|
||||||
dashboard.delete_repo_archives=Verwijderen van alle repositories archieven
|
dashboard.delete_repo_archives=Verwijderen van alle repositories archieven
|
||||||
dashboard.delete_repo_archives_success=Alle repositories archieven hebben verwijderd.
|
dashboard.delete_repo_archives_success=Alle repositories archieven hebben verwijderd.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Garbage collectie uitvoeren
|
dashboard.git_gc_repos=Garbage collectie uitvoeren
|
||||||
dashboard.git_gc_repos_success=Garbage collectie met succes uitgevoerd.
|
dashboard.git_gc_repos_success=Garbage collectie met succes uitgevoerd.
|
||||||
dashboard.resync_all_sshkeys=Herschrijf '.ssh/authorized_keys' (Let op: alle sleutels die niet van Gogs zijn zullen verloren gaan!)
|
dashboard.resync_all_sshkeys=Herschrijf '.ssh/authorized_keys' (Let op: alle sleutels die niet van Gogs zijn zullen verloren gaan!)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Bewerk account
|
||||||
users.is_activated=Dit account is geactiveerd
|
users.is_activated=Dit account is geactiveerd
|
||||||
users.is_admin=Dit account heeft beheerdersrechten
|
users.is_admin=Dit account heeft beheerdersrechten
|
||||||
users.allow_git_hook=Deze account beschikt over machtigingen voor het maken van Git haken
|
users.allow_git_hook=Deze account beschikt over machtigingen voor het maken van Git haken
|
||||||
|
users.allow_import_local=This account has permissions to import local repositories
|
||||||
users.update_profile=Account profiel bijwerken
|
users.update_profile=Account profiel bijwerken
|
||||||
users.delete_account=Dit account verwijderen
|
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_own_repo=Dit account is nog steeds eigendom van een repositorie. U moet deze repositorie eerst verwijderen of overdragen.
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=Systeem bericht is met succes verwijderd.
|
||||||
[action]
|
[action]
|
||||||
create_repo=repositorie aangemaakt in <a href="%s">%s</a>
|
create_repo=repositorie aangemaakt in <a href="%s">%s</a>
|
||||||
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=push update naar <a href="%s/src/%s">%[2]s</a> in <a href="%[1]s">%[3]s</a>
|
commit_repo=push update naar <a href="%[1]s/src/%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`opende issue in <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`opende issue in <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=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`reactie op issue <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue=`reactie op issue <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
|
|
|
@ -111,7 +111,7 @@ admin_title=Ustawienia konta administratora
|
||||||
admin_name=Nazwa Użytkownika
|
admin_name=Nazwa Użytkownika
|
||||||
admin_password=Hasło
|
admin_password=Hasło
|
||||||
confirm_password=Potwierdź hasło
|
confirm_password=Potwierdź hasło
|
||||||
admin_email=E-mail
|
admin_email=Admin E-mail
|
||||||
install_gogs=Zainstaluj Gogs
|
install_gogs=Zainstaluj Gogs
|
||||||
test_git_failed=Nie udało się przetestować polecenia "git": %v
|
test_git_failed=Nie udało się przetestować polecenia "git": %v
|
||||||
sqlite3_not_available=Twoje wydanie nie obsługuje SQLite3, proszę pobrać oficjalne wydanie z %s, a NIE wersję z gobuild.
|
sqlite3_not_available=Twoje wydanie nie obsługuje SQLite3, proszę pobrać oficjalne wydanie z %s, a NIE wersję z gobuild.
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Zapomniałem hasła
|
||||||
forget_password=Zapomniałeś hasła?
|
forget_password=Zapomniałeś hasła?
|
||||||
sign_up_now=Potrzebujesz konta? Zarejestruj się teraz.
|
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.
|
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_to_account=Zaloguj się na swoje konto
|
|
||||||
active_your_account=Aktywuj swoje konto
|
active_your_account=Aktywuj swoje konto
|
||||||
resent_limit_prompt=Niestety, zbyt często wysyłasz e-mail aktywacyjny. Proszę odczekać 3 minuty.
|
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.
|
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.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Prosimy aktywować swoje konto
|
||||||
activate_email=Sprawdź Twój adres e-mail
|
activate_email=Sprawdź Twój adres e-mail
|
||||||
reset_password=Zmień swoje hasło
|
reset_password=Zmień swoje hasło
|
||||||
register_success=Zostałeś zarejestrowany, witamy
|
register_success=Zostałeś zarejestrowany, witamy
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Tak
|
yes=Tak
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` musi zawierać co najwyżej %s znaków.`
|
||||||
max_size_error=` musi zawierać co najwyżej %s znaków.`
|
max_size_error=` musi zawierać co najwyżej %s znaków.`
|
||||||
email_error=` nie jest poprawnym adresem e-mail.`
|
email_error=` nie jest poprawnym adresem e-mail.`
|
||||||
url_error=` nie jest poprawnym adresem URL.`
|
url_error=` nie jest poprawnym adresem URL.`
|
||||||
|
include_error=` must contain substring '%s'.`
|
||||||
unknown_error=Nieznany błąd:
|
unknown_error=Nieznany błąd:
|
||||||
captcha_incorrect=Kod captcha nie zgadza się.
|
captcha_incorrect=Kod captcha nie zgadza się.
|
||||||
password_not_match=Hasło i potwierdzenie nie zgadzają się.
|
password_not_match=Hasło i potwierdzenie nie zgadzają się.
|
||||||
|
@ -336,6 +337,7 @@ visibility=Widoczność
|
||||||
visiblity_helper=To repozytorium jest <span class="ui red text">prywatne</span>
|
visiblity_helper=To repozytorium jest <span class="ui red text">prywatne</span>
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
||||||
visiblity_fork_helper=(Change of this value will affect all forks)
|
visiblity_fork_helper=(Change of this value will affect all forks)
|
||||||
|
clone_helper=Potrzebujesz pomocy z klonowaniem? Odwiedź <a target="_blank" href="%s">Pomoc</a>!
|
||||||
fork_repo=Sforkowane
|
fork_repo=Sforkowane
|
||||||
fork_from=Forkuj z
|
fork_from=Forkuj z
|
||||||
fork_visiblity_helper=Fork nie może zmieniać swojej widoczności
|
fork_visiblity_helper=Fork nie może zmieniać swojej widoczności
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Initialize this repository with selected files and template
|
||||||
create_repo=Utwórz repozytorium
|
create_repo=Utwórz repozytorium
|
||||||
default_branch=Domyślna gałąź
|
default_branch=Domyślna gałąź
|
||||||
mirror_interval=Odświeżanie mirrorów (godziny)
|
mirror_interval=Odświeżanie mirrorów (godziny)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Nazwa repozytorium "%s" jest zarezerwowana.
|
form.name_reserved=Nazwa repozytorium "%s" jest zarezerwowana.
|
||||||
form.name_pattern_not_allowed=Wzorzec nazwy repozytorium "%s" jest niedozwolony.
|
form.name_pattern_not_allowed=Wzorzec nazwy repozytorium "%s" jest niedozwolony.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=This repository will be a <span class="text blue">mirror</sp
|
||||||
migrate_repo=Przenieś repozytorium
|
migrate_repo=Przenieś repozytorium
|
||||||
migrate.clone_address=Sklonuj adres
|
migrate.clone_address=Sklonuj adres
|
||||||
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
|
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
|
||||||
|
migrate.permission_denied=You are not allowed to import local repositories.
|
||||||
migrate.invalid_local_path=Ścieżka jest niepoprawna. Nie istnieje lub nie jest katalogiem.
|
migrate.invalid_local_path=Ścieżka jest niepoprawna. Nie istnieje lub nie jest katalogiem.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=sklonowany z
|
forked_from=sklonowany z
|
||||||
fork_from_self=You cannot fork repository you already owned!
|
fork_from_self=You cannot fork repository you already owned!
|
||||||
copy_link=Kopiuj
|
copy_link=Kopiuj
|
||||||
copy_link_success=Skopiowane!
|
copy_link_success=Skopiowane!
|
||||||
copy_link_error=Naciśnij klawisze ⌘-C i Ctrl-C, aby skopiować
|
copy_link_error=Naciśnij klawisze ⌘-C i Ctrl-C, aby skopiować
|
||||||
click_to_copy=Kopiuj do schowka
|
|
||||||
copied=Skopiowano
|
copied=Skopiowano
|
||||||
clone_helper=Potrzebujesz pomocy z klonowaniem? Odwiedź <a target="_blank" href="%s">Pomoc</a>!
|
|
||||||
unwatch=Przestań obserwować
|
unwatch=Przestań obserwować
|
||||||
watch=Obserwuj
|
watch=Obserwuj
|
||||||
unstar=Usuń gwiazdkę
|
unstar=Usuń gwiazdkę
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=Utwórz nowe repozytorium z wiersza poleceń
|
||||||
push_exist_repo=Wyślij istniejące repozytorium z wiersza poleceń
|
push_exist_repo=Wyślij istniejące repozytorium z wiersza poleceń
|
||||||
repo_is_empty=To repozytorium jest puste, proszę wrócić później!
|
repo_is_empty=To repozytorium jest puste, proszę wrócić później!
|
||||||
|
|
||||||
|
|
||||||
branch=Gałąź
|
branch=Gałąź
|
||||||
tree=Drzewo
|
tree=Drzewo
|
||||||
branch_and_tags=Gałęzie i tagi
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Gałęzie
|
branches=Gałęzie
|
||||||
tags=Tagi
|
tags=Tagi
|
||||||
issues=Problemy
|
issues=Problemy
|
||||||
|
@ -454,9 +458,9 @@ issues.closed_title=zamknięty
|
||||||
issues.num_comments=%d komentarzy
|
issues.num_comments=%d komentarzy
|
||||||
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content=There is no content yet.
|
issues.no_content=There is no content yet.
|
||||||
issues.close_issue=Close
|
issues.close_issue=Zamknij
|
||||||
issues.close_comment_issue=Close and comment
|
issues.close_comment_issue=Skomentuj i zamknij
|
||||||
issues.reopen_issue=Reopen
|
issues.reopen_issue=Otwórz ponownie
|
||||||
issues.reopen_comment_issue=Otwórz ponownie i dodaj komentarz
|
issues.reopen_comment_issue=Otwórz ponownie i dodaj komentarz
|
||||||
issues.create_comment=Komentuj
|
issues.create_comment=Komentuj
|
||||||
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
@ -481,11 +485,12 @@ issues.label_deletion=Usunięcie etykiety
|
||||||
issues.label_deletion_desc=Delete this label will remove its information in all related issues. Do you want to continue?
|
issues.label_deletion_desc=Delete this label will remove its information in all related issues. Do you want to continue?
|
||||||
issues.label_deletion_success=Etykieta została usunięta pomyślnie!
|
issues.label_deletion_success=Etykieta została usunięta pomyślnie!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Compare Changes
|
pulls.compare_changes=Compare Changes
|
||||||
pulls.compare_changes_desc=Compare two branches and make a pull request for changes.
|
pulls.compare_changes_desc=Compare two branches and make a pull request for changes.
|
||||||
pulls.compare_base=base
|
pulls.compare_base=base
|
||||||
pulls.compare_compare=compare
|
pulls.compare_compare=compare
|
||||||
pulls.filter_branch=Filter branch
|
pulls.filter_branch=Filtruj branch
|
||||||
pulls.no_results=Nie znaleziono wyników.
|
pulls.no_results=Nie znaleziono wyników.
|
||||||
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even.
|
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.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>`
|
||||||
|
@ -493,17 +498,17 @@ pulls.create=Utwórz Pull Request
|
||||||
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code>
|
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.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
|
||||||
pulls.tab_conversation=Conversation
|
pulls.tab_conversation=Conversation
|
||||||
pulls.tab_commits=Commits
|
pulls.tab_commits=Commity
|
||||||
pulls.tab_files=Files changed
|
pulls.tab_files=Pliki zmodyfikowane
|
||||||
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
|
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
|
||||||
pulls.merged=Merged
|
pulls.merged=Scalone
|
||||||
pulls.has_merged=This pull request has been merged successfully!
|
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.data_broken=Data of this pull request has been broken due to deletion of fork information.
|
||||||
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
|
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
|
||||||
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
|
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_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.cannot_auto_merge_helper=Please use command line tool to solve it.
|
||||||
pulls.merge_pull_request=Merge Pull Request
|
pulls.merge_pull_request=Scal Pull Request
|
||||||
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
|
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
|
||||||
|
|
||||||
milestones.new=Nowy kamień milowy
|
milestones.new=Nowy kamień milowy
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=Tytuł
|
||||||
milestones.desc=Opis
|
milestones.desc=Opis
|
||||||
milestones.due_date=Termin realizacji (opcjonalnie)
|
milestones.due_date=Termin realizacji (opcjonalnie)
|
||||||
milestones.clear=Wyczyść
|
milestones.clear=Wyczyść
|
||||||
milestones.invalid_due_date_format=Format daty realizacji jest nieprawidłowy, musi być "rok mm-dd".
|
milestones.invalid_due_date_format=Format daty realizacji jest nieprawidłowy, musi być "rrrr-mm-dd".
|
||||||
milestones.create_success=Kamień milowy "%s" został utworzony pomyślnie!
|
milestones.create_success=Kamień milowy "%s" został utworzony pomyślnie!
|
||||||
milestones.edit=Edytuj kamień milowy
|
milestones.edit=Edytuj kamień milowy
|
||||||
milestones.edit_subheader=Use better description for milestones so people won't be confused.
|
milestones.edit_subheader=Use better description for milestones so people won't be confused.
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Potwierdź usunięcie
|
||||||
settings.add_collaborator=Dodaj nowego współpracownika
|
settings.add_collaborator=Dodaj nowego współpracownika
|
||||||
settings.add_collaborator_success=Został dodany nowy współpracownik.
|
settings.add_collaborator_success=Został dodany nowy współpracownik.
|
||||||
settings.remove_collaborator_success=Współpracownik został usunięty.
|
settings.remove_collaborator_success=Współpracownik został usunięty.
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=Użytkownik jest członkiem organizacji, który nie może być dodany jako współpracownik.
|
settings.user_is_org_member=Użytkownik jest członkiem organizacji, który nie może być dodany jako współpracownik.
|
||||||
settings.add_webhook=Dodaj Webhooka
|
settings.add_webhook=Dodaj Webhooka
|
||||||
settings.hooks_desc=Webhooks are much like basic HTTP POST event triggers. Whenever something occurs in Gogs, we will handle the notification to the target host you specify. Learn more in this <a target="_blank" href="%s">Webhooks Guide</a>.
|
settings.hooks_desc=Webhooks are much like basic HTTP POST event triggers. Whenever something occurs in Gogs, we will handle the notification to the target host you specify. Learn more in this <a target="_blank" href="%s">Webhooks Guide</a>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Stabilny
|
||||||
release.edit=edytuj
|
release.edit=edytuj
|
||||||
release.ahead=<strong>%d</strong> commitów w %s od tego wydania
|
release.ahead=<strong>%d</strong> commitów w %s od tego wydania
|
||||||
release.source_code=Kod źródłowy
|
release.source_code=Kod źródłowy
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Nazwa tagu
|
release.tag_name=Nazwa tagu
|
||||||
release.target=Cel
|
release.target=Cel
|
||||||
release.tag_helper=Wybierz istniejący tag, bądź utwórz nowy podczas publikacji.
|
release.tag_helper=Wybierz istniejący tag, bądź utwórz nowy podczas publikacji.
|
||||||
release.release_title=Nazwa wydania
|
release.title=Title
|
||||||
release.content_with_md=Treść sformatowana za pomocą <a href="%s">Markdown</a>
|
release.content=Content
|
||||||
release.write=Napisz
|
release.write=Napisz
|
||||||
release.preview=Pogdląd
|
release.preview=Pogdląd
|
||||||
release.content_placeholder=Napisz jakąś treść
|
|
||||||
release.loading=Ładowanie...
|
release.loading=Ładowanie...
|
||||||
release.prerelease_desc=To jest wersja wstępna
|
release.prerelease_desc=To jest wersja wstępna
|
||||||
release.prerelease_helper=Chcemy zwrócić uwagę, że ta wersja jest oznaczona jako eksperymentalna.
|
release.prerelease_helper=Chcemy zwrócić uwagę, że ta wersja jest oznaczona jako eksperymentalna.
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=Publikuj wersję
|
release.publish=Publikuj wersję
|
||||||
release.save_draft=Zapisz szkic
|
release.save_draft=Zapisz szkic
|
||||||
release.edit_release=Edytuj wydanie
|
release.edit_release=Edytuj wydanie
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Wersja o tej nazwie tagu już istnieje.
|
release.tag_name_already_exist=Wersja o tej nazwie tagu już istnieje.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Nazwa organizacji
|
org_name_holder=Nazwa organizacji
|
||||||
|
org_full_name_holder=Organization Full Name
|
||||||
org_name_helper=Świetne nazwy organizacji są krótkie i łatwe do zapamiętania.
|
org_name_helper=Świetne nazwy organizacji są krótkie i łatwe do zapamiętania.
|
||||||
create_org=Utwórz organizację
|
create_org=Utwórz organizację
|
||||||
repo_updated=Zaktualizowano
|
repo_updated=Zaktualizowano
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=Usuń wszystkie nieaktywne konta
|
||||||
dashboard.delete_inactivate_accounts_success=Wszystkie nieaktywne konta zostały usunięte pomyślnie.
|
dashboard.delete_inactivate_accounts_success=Wszystkie nieaktywne konta zostały usunięte pomyślnie.
|
||||||
dashboard.delete_repo_archives=Usuń wszystkie archiwa repozytoriów
|
dashboard.delete_repo_archives=Usuń wszystkie archiwa repozytoriów
|
||||||
dashboard.delete_repo_archives_success=Pomyślnie usunięto wszystkie archiwa repozytoriów.
|
dashboard.delete_repo_archives_success=Pomyślnie usunięto wszystkie archiwa repozytoriów.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Usuń śmieci z repozytoriów
|
dashboard.git_gc_repos=Usuń śmieci z repozytoriów
|
||||||
dashboard.git_gc_repos_success=Wszystkie repozytoria zakończyły odśmiecanie pomyślnie.
|
dashboard.git_gc_repos_success=Wszystkie repozytoria zakończyły odśmiecanie pomyślnie.
|
||||||
dashboard.resync_all_sshkeys=Przeładuj klucze publiczne w pliku '.ssh/authorized_keys' (uwaga: klucze poza Gogs zostaną usunięte)
|
dashboard.resync_all_sshkeys=Przeładuj klucze publiczne w pliku '.ssh/authorized_keys' (uwaga: klucze poza Gogs zostaną usunięte)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Edytuj konto
|
||||||
users.is_activated=To konto jest aktywne
|
users.is_activated=To konto jest aktywne
|
||||||
users.is_admin=To konto ma uprawnienia administratora
|
users.is_admin=To konto ma uprawnienia administratora
|
||||||
users.allow_git_hook=To konto posiada uprawnienia do tworzenia skryptów Git
|
users.allow_git_hook=To konto posiada uprawnienia do tworzenia skryptów Git
|
||||||
|
users.allow_import_local=This account has permissions to import local repositories
|
||||||
users.update_profile=Zaktualizuj profil konta
|
users.update_profile=Zaktualizuj profil konta
|
||||||
users.delete_account=Usuń to konto
|
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_own_repo=Twoje konto jest dalej właścicielem repozytorium, musisz je usunąć lub przekazać.
|
||||||
|
@ -851,22 +868,22 @@ auths.ms_ad_sa=Ms Ad SA
|
||||||
auths.smtp_auth=SMTP Authentication Type
|
auths.smtp_auth=SMTP Authentication Type
|
||||||
auths.smtphost=Serwer SMTP
|
auths.smtphost=Serwer SMTP
|
||||||
auths.smtpport=Port SMTP
|
auths.smtpport=Port SMTP
|
||||||
auths.allowed_domains=Allowed Domains
|
auths.allowed_domains=Dozwolone domeny
|
||||||
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
|
auths.allowed_domains_helper=Pozostaw puste aby nie ograniczać domen. Wiele domen powinno być oddzielone przecinkami ','.
|
||||||
auths.enable_tls=Włącz szyfrowanie TLS
|
auths.enable_tls=Włącz szyfrowanie TLS
|
||||||
auths.skip_tls_verify=Pomiń weryfikację protokołu TLS
|
auths.skip_tls_verify=Pomiń weryfikację protokołu TLS
|
||||||
auths.pam_service_name=Nazwa usługi PAM
|
auths.pam_service_name=Nazwa usługi PAM
|
||||||
auths.enable_auto_register=Włącz automatyczną rejestrację
|
auths.enable_auto_register=Włącz automatyczną rejestrację
|
||||||
auths.tips=Wskazówki
|
auths.tips=Wskazówki
|
||||||
auths.edit=Edit Authentication Setting
|
auths.edit=Edytuj ustawienia uwierzytelniania
|
||||||
auths.activated=To uwierzytelnienie zostało aktywowane
|
auths.activated=To uwierzytelnienie zostało aktywowane
|
||||||
auths.new_success=New authentication '%s' has been added successfully.
|
auths.new_success=Pomyślnie dodano nowe uwierzytelnianie '%s'.
|
||||||
auths.update_success=Ustawienia uwierzytelnienia zostały zaktualizowane pomyślnie.
|
auths.update_success=Ustawienia uwierzytelnienia zostały zaktualizowane pomyślnie.
|
||||||
auths.update=Aktualizuj ustawienia uwierzytelniania
|
auths.update=Aktualizuj ustawienia uwierzytelniania
|
||||||
auths.delete=Delete This Authentication
|
auths.delete=Usuń to uwierzytelnianie
|
||||||
auths.delete_auth_title=Authentication Deletion
|
auths.delete_auth_title=Usunięcie uwierzytelniania
|
||||||
auths.delete_auth_desc=To uwierzytelnienie zostanie usunięte, czy chcesz kontynuować?
|
auths.delete_auth_desc=To uwierzytelnienie zostanie usunięte, czy chcesz kontynuować?
|
||||||
auths.deletion_success=Authentication has been deleted successfully!
|
auths.deletion_success=Uwierzytelnianie zostało usunięte pomyślnie!
|
||||||
|
|
||||||
config.server_config=Konfiguracja serwera
|
config.server_config=Konfiguracja serwera
|
||||||
config.app_name=Nazwa Aplikacji
|
config.app_name=Nazwa Aplikacji
|
||||||
|
@ -953,12 +970,12 @@ notices.delete_success=Powiadomienia systemowe zostały usunięte pomyślnie.
|
||||||
|
|
||||||
[action]
|
[action]
|
||||||
create_repo=utworzono repozytorium <a href="%s"> %s</a>
|
create_repo=utworzono repozytorium <a href="%s"> %s</a>
|
||||||
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
rename_repo=nazwa repozytorium zmieniona z <code>%[1]s</code> na <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=wypchnął do <a href="%s/src/%s">%[2]s</a> w <a href="%[1]s"> %[3]s</a>
|
commit_repo=wypchnął do <a href="%[1]s/src/%[2]s">%[3]s</a> w <a href="%[1]s"> %[4]s</a>
|
||||||
create_issue=`zgłosił problem <a href="%s/issues/%s">#%[2]s %[3]s</a>`
|
create_issue=`zgłosił problem <a href="%s/issues/%s">#%[2]s %[3]s</a>`
|
||||||
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request="stworzył pull request <a href="%s/pulls/%s"> %s #%[2]s"</a>
|
||||||
comment_issue=`skomentował problem <a href="%s/issues/%s">#%[2]s %[3]s</a>`
|
comment_issue=`skomentował problem <a href="%s/issues/%s">#%[2]s %[3]s</a>`
|
||||||
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
merge_pull_request=scalił pull request <a href="%s/pulls/%s"> %s #%[2]s"</a>
|
||||||
transfer_repo=przeniósł repozytorium <code>%s</code> do <a href="%s">%s</a>
|
transfer_repo=przeniósł repozytorium <code>%s</code> do <a href="%s">%s</a>
|
||||||
push_tag=opublikował tag <a href="%s/src/%s">%[2]s</a> w <a href="%[1]s">%[3]s</a>
|
push_tag=opublikował tag <a href="%s/src/%s">%[2]s</a> w <a href="%[1]s">%[3]s</a>
|
||||||
compare_2_commits=Zobacz porównanie tych 2 commitów
|
compare_2_commits=Zobacz porównanie tych 2 commitów
|
||||||
|
|
|
@ -111,7 +111,7 @@ admin_title=Configurações da Conta de Administrador
|
||||||
admin_name=Nome de Usuário
|
admin_name=Nome de Usuário
|
||||||
admin_password=Senha
|
admin_password=Senha
|
||||||
confirm_password=Confirmar Senha
|
confirm_password=Confirmar Senha
|
||||||
admin_email=E-mail
|
admin_email=E-mail do Administrador
|
||||||
install_gogs=Instalar Gogs
|
install_gogs=Instalar Gogs
|
||||||
test_git_failed=Falha ao testar o comando 'git': %v
|
test_git_failed=Falha ao testar o comando 'git': %v
|
||||||
sqlite3_not_available=Sua versão não suporta SQLite3, por favor faça o download da versão binária oficial em %s, NÃO da versão gobuild.
|
sqlite3_not_available=Sua versão não suporta SQLite3, por favor faça o download da versão binária oficial em %s, NÃO da versão gobuild.
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Esqueci a Senha
|
||||||
forget_password=Esqueceu a senha?
|
forget_password=Esqueceu a senha?
|
||||||
sign_up_now=Precisa de uma conta? Cadastre-se agora.
|
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.
|
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_to_account=Entre com sua conta
|
|
||||||
active_your_account=Ativar Sua Conta
|
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.
|
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.
|
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.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Por favor, ative sua conta
|
||||||
activate_email=Verifique seu endereço de e-mail
|
activate_email=Verifique seu endereço de e-mail
|
||||||
reset_password=Resetar sua senha
|
reset_password=Resetar sua senha
|
||||||
register_success=Registrado com sucesso. Bem vindo
|
register_success=Registrado com sucesso. Bem vindo
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Sim
|
yes=Sim
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=` deve conter pelo menos %s caracteres.`
|
||||||
max_size_error=` deve conter no máximo %s caracteres.`
|
max_size_error=` deve conter no máximo %s caracteres.`
|
||||||
email_error=` não é um endereço de e-mail válido.`
|
email_error=` não é um endereço de e-mail válido.`
|
||||||
url_error=`não é uma URL válida.`
|
url_error=`não é uma URL válida.`
|
||||||
|
include_error=` deve conter '%s'.`
|
||||||
unknown_error=Erro desconhecido:
|
unknown_error=Erro desconhecido:
|
||||||
captcha_incorrect=O captcha não correspondeu.
|
captcha_incorrect=O captcha não correspondeu.
|
||||||
password_not_match=Senha e confirmação da senha não são as mesmas.
|
password_not_match=Senha e confirmação da senha não são as mesmas.
|
||||||
|
@ -336,6 +337,7 @@ visibility=Visibilidade
|
||||||
visiblity_helper=Este é um repositório <span class="ui red text"> privado</span>
|
visiblity_helper=Este é um repositório <span class="ui red text"> privado</span>
|
||||||
visiblity_helper_forced=O adminstrador forçou todos os novos repositórios para serem <span class="ui red text">Privados</span>
|
visiblity_helper_forced=O adminstrador forçou todos os novos repositórios para serem <span class="ui red text">Privados</span>
|
||||||
visiblity_fork_helper=(A alteração desse valor irá afetar todos os forks)
|
visiblity_fork_helper=(A alteração desse valor irá afetar todos os forks)
|
||||||
|
clone_helper=Precisa de ajuda com a clonagem? Visite a <a target="_blank" href="%s">Ajuda</a>!
|
||||||
fork_repo=Fork o Repositório
|
fork_repo=Fork o Repositório
|
||||||
fork_from=Fork de
|
fork_from=Fork de
|
||||||
fork_visiblity_helper=Não é possível alterar a visibilidade de um repositório forkado.
|
fork_visiblity_helper=Não é possível alterar a visibilidade de um repositório forkado.
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Inicializar este repositório com os arquivos selecionados e modelo
|
||||||
create_repo=Criar Repositório
|
create_repo=Criar Repositório
|
||||||
default_branch=Branch padrão
|
default_branch=Branch padrão
|
||||||
mirror_interval=Intervalo de Espelho (hora)
|
mirror_interval=Intervalo de Espelho (hora)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=O nome de repositório '%s' não pode ser usado.
|
form.name_reserved=O nome de repositório '%s' não pode ser usado.
|
||||||
form.name_pattern_not_allowed=Não é permitido usar o padrão '%s' para o nome de repositório.
|
form.name_pattern_not_allowed=Não é permitido usar o padrão '%s' para o nome de repositório.
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=Este repositório será um <span class="text blue"> espelho<
|
||||||
migrate_repo=Migrar Repositório
|
migrate_repo=Migrar Repositório
|
||||||
migrate.clone_address=Endereço de Clone
|
migrate.clone_address=Endereço de Clone
|
||||||
migrate.clone_address_desc=Isto pode ser uma URL de HTTP/HTTPS/GIT ou um caminho de diretório local.
|
migrate.clone_address_desc=Isto pode ser uma URL de HTTP/HTTPS/GIT ou um caminho de diretório local.
|
||||||
|
migrate.permission_denied=Você não pode importar repositórios locais.
|
||||||
migrate.invalid_local_path=Caminho local inválido, não existe ou não é um diretório.
|
migrate.invalid_local_path=Caminho local inválido, não existe ou não é um diretório.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=forkado de
|
forked_from=forkado de
|
||||||
fork_from_self=Você não pode criar fork de um repositório que já é seu!
|
fork_from_self=Você não pode criar fork de um repositório que já é seu!
|
||||||
copy_link=Copiar
|
copy_link=Copiar
|
||||||
copy_link_success=Copiado!
|
copy_link_success=Copiado!
|
||||||
copy_link_error=Pressione ⌘-C ou Ctrl-C para copiar
|
copy_link_error=Pressione ⌘-C ou Ctrl-C para copiar
|
||||||
click_to_copy=Copiar para a área de transferência
|
|
||||||
copied=Copiado com sucesso
|
copied=Copiado com sucesso
|
||||||
clone_helper=Precisa de ajuda com a clonagem? Visite a <a target="_blank" href="%s">Ajuda</a>!
|
|
||||||
unwatch=Deixar de Observar
|
unwatch=Deixar de Observar
|
||||||
watch=Observar
|
watch=Observar
|
||||||
unstar=Remover favorito
|
unstar=Remover favorito
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=Criar um novo repositório na linha de comando
|
||||||
push_exist_repo=Push um repositório existente na linha de comando
|
push_exist_repo=Push um repositório existente na linha de comando
|
||||||
repo_is_empty=Este repositório está vazio, por favor volte mais tarde!
|
repo_is_empty=Este repositório está vazio, por favor volte mais tarde!
|
||||||
|
|
||||||
|
|
||||||
branch=Branch
|
branch=Branch
|
||||||
tree=Árvore
|
tree=Árvore
|
||||||
branch_and_tags=Branches & Tags
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Branches
|
branches=Branches
|
||||||
tags=Tags
|
tags=Tags
|
||||||
issues=Problemas
|
issues=Problemas
|
||||||
|
@ -457,7 +461,7 @@ issues.no_content=Nenhum conteúdo textual.
|
||||||
issues.close_issue=Fechar
|
issues.close_issue=Fechar
|
||||||
issues.close_comment_issue=Comentar e fechar
|
issues.close_comment_issue=Comentar e fechar
|
||||||
issues.reopen_issue=Reabrir
|
issues.reopen_issue=Reabrir
|
||||||
issues.reopen_comment_issue=Reabrir e comentar
|
issues.reopen_comment_issue=Comentar e reabrir
|
||||||
issues.create_comment=Comentar
|
issues.create_comment=Comentar
|
||||||
issues.closed_at=`fechado em <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at=`fechado em <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at=`reaberto em <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at=`reaberto em <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=Exclusão de etiqueta
|
||||||
issues.label_deletion_desc=Excluir uma etiqueta a retirará de todos os problemas que ela estiver marcando. Quer mesmo continuar?
|
issues.label_deletion_desc=Excluir uma etiqueta a retirará de todos os problemas que ela estiver marcando. Quer mesmo continuar?
|
||||||
issues.label_deletion_success=A etiqueta foi excluída com sucesso!
|
issues.label_deletion_success=A etiqueta foi excluída com sucesso!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Comparar mudanças
|
pulls.compare_changes=Comparar mudanças
|
||||||
pulls.compare_changes_desc=Comparar os dois branches e criar pull request com as mudanças.
|
pulls.compare_changes_desc=Comparar os dois branches e criar pull request com as mudanças.
|
||||||
pulls.compare_base=base
|
pulls.compare_base=base
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=Confirmar Deleção
|
||||||
settings.add_collaborator=Adicionar um Novo Colaborador
|
settings.add_collaborator=Adicionar um Novo Colaborador
|
||||||
settings.add_collaborator_success=O novo colaborador foi adicionado.
|
settings.add_collaborator_success=O novo colaborador foi adicionado.
|
||||||
settings.remove_collaborator_success=O colaborador foi removido.
|
settings.remove_collaborator_success=O colaborador foi removido.
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=O usuário é um membro da organização que não pode ser adicionado como um colaborador.
|
settings.user_is_org_member=O usuário é um membro da organização que não pode ser adicionado como um colaborador.
|
||||||
settings.add_webhook=Adicionar Webhook
|
settings.add_webhook=Adicionar Webhook
|
||||||
settings.hooks_desc=Hooks da web ou Webhooks permitem serviços externos serem notificados quando certos eventos acontecem no Gogs. Quando acontecem os eventos especificados, enviaremos uma solicitação POST para cada uma das URLs que você fornecer. Saiba mais no nosso <a target="_blank" href="%s"> Guia de Webhooks</a>.
|
settings.hooks_desc=Hooks da web ou Webhooks permitem serviços externos serem notificados quando certos eventos acontecem no Gogs. Quando acontecem os eventos especificados, enviaremos uma solicitação POST para cada uma das URLs que você fornecer. Saiba mais no nosso <a target="_blank" href="%s"> Guia de Webhooks</a>.
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Estável
|
||||||
release.edit=editar
|
release.edit=editar
|
||||||
release.ahead=<strong>%d</strong> commits para %s depois desta versão
|
release.ahead=<strong>%d</strong> commits para %s depois desta versão
|
||||||
release.source_code=Código fonte
|
release.source_code=Código fonte
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Nome da tag
|
release.tag_name=Nome da tag
|
||||||
release.target=Destino
|
release.target=Destino
|
||||||
release.tag_helper=Escolha uma tag existente, ou crie uma nova tag em publicar.
|
release.tag_helper=Escolha uma tag existente, ou crie uma nova tag em publicar.
|
||||||
release.release_title=Título da Versão
|
release.title=Title
|
||||||
release.content_with_md=Conteúdo com <a href="%s">Markdown</a>
|
release.content=Content
|
||||||
release.write=Escrever
|
release.write=Escrever
|
||||||
release.preview=Visualizar
|
release.preview=Visualizar
|
||||||
release.content_placeholder=Escreva algum conteúdo
|
|
||||||
release.loading=Carregando...
|
release.loading=Carregando...
|
||||||
release.prerelease_desc=Esta é uma versão prévia
|
release.prerelease_desc=Esta é uma versão prévia
|
||||||
release.prerelease_helper=Vou salientar que esta versão é identificada como não pronta para produção.
|
release.prerelease_helper=Vou salientar que esta versão é identificada como não pronta para produção.
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=Publicar Versão
|
release.publish=Publicar Versão
|
||||||
release.save_draft=Salvar Rascunho
|
release.save_draft=Salvar Rascunho
|
||||||
release.edit_release=Editar Versão
|
release.edit_release=Editar Versão
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Já existiu versão com esse nome de tag.
|
release.tag_name_already_exist=Já existiu versão com esse nome de tag.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Nome da Organização
|
org_name_holder=Nome da Organização
|
||||||
|
org_full_name_holder=Nome completo da organização
|
||||||
org_name_helper=Nomes de grandes organizações são curtos e memoráveis.
|
org_name_helper=Nomes de grandes organizações são curtos e memoráveis.
|
||||||
create_org=Criar Organização
|
create_org=Criar Organização
|
||||||
repo_updated=Atualizado
|
repo_updated=Atualizado
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=Excluir todas as contas inativas
|
||||||
dashboard.delete_inactivate_accounts_success=Todas as contas inativas foram excluídas com sucesso.
|
dashboard.delete_inactivate_accounts_success=Todas as contas inativas foram excluídas com sucesso.
|
||||||
dashboard.delete_repo_archives=Excluir todos os arquivos dos repositórios
|
dashboard.delete_repo_archives=Excluir todos os arquivos dos repositórios
|
||||||
dashboard.delete_repo_archives_success=Todos os arquivos dos repositórios foram excluídos com sucesso.
|
dashboard.delete_repo_archives_success=Todos os arquivos dos repositórios foram excluídos com sucesso.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Fazer coleta de lixo nos repositórios
|
dashboard.git_gc_repos=Fazer coleta de lixo nos repositórios
|
||||||
dashboard.git_gc_repos_success=Em todos repositórios, a coleta de lixo foi realizada com sucesso.
|
dashboard.git_gc_repos_success=Em todos repositórios, a coleta de lixo foi realizada com sucesso.
|
||||||
dashboard.resync_all_sshkeys=Reescrever o arquivo '.ssh/authorized_keys' (atenção: chaves que não sejam do Gogs serão perdidas)
|
dashboard.resync_all_sshkeys=Reescrever o arquivo '.ssh/authorized_keys' (atenção: chaves que não sejam do Gogs serão perdidas)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=Editar Conta
|
||||||
users.is_activated=Esta conta está ativada
|
users.is_activated=Esta conta está ativada
|
||||||
users.is_admin=Esta conta tem permissões de administrador
|
users.is_admin=Esta conta tem permissões de administrador
|
||||||
users.allow_git_hook=Esta conta tem permissões para criar hooks do Git
|
users.allow_git_hook=Esta conta tem permissões para criar hooks do Git
|
||||||
|
users.allow_import_local=Esta conta tem permissões para importar repositórios locais
|
||||||
users.update_profile=Atualizar Perfil da Conta
|
users.update_profile=Atualizar Perfil da Conta
|
||||||
users.delete_account=Deletar Esta 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_own_repo=Sua conta ainda é proprietária do repositório, você tem que excluir ou transferi-lo primeiro.
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=Aviso do sistema foi deletado com sucesso.
|
||||||
[action]
|
[action]
|
||||||
create_repo=repositório criado <a href="%s"> %s</a>
|
create_repo=repositório criado <a href="%s"> %s</a>
|
||||||
rename_repo=renomeou o o repositório <code>%[1]s</code> para <a href="%[2]s">%[3]s</a>
|
rename_repo=renomeou o o repositório <code>%[1]s</code> para <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=pushed para <a href="%s/src/%s">%[2]s</a> em <a href="%[1]s">%[3]s</a>
|
commit_repo=pushed para <a href="%[1]s/src/%[2]s">%[3]s</a> em <a href="%[1]s">%[4]s</a>
|
||||||
create_issue='questão aberta <a href="%s/issues/%s">%s#%[2]s</a>'
|
create_issue='questão aberta <a href="%s/issues/%s">%s#%[2]s</a>'
|
||||||
create_pull_request=`criou o pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`criou o pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue='comentou sobre a questão <a href="%s/issues/%s">%s#%[2]s</a>'
|
comment_issue='comentou sobre a questão <a href="%s/issues/%s">%s#%[2]s</a>'
|
||||||
|
|
|
@ -99,7 +99,7 @@ server_service_title=Сервер и другие настройки служб
|
||||||
offline_mode=Включение офлайн режима
|
offline_mode=Включение офлайн режима
|
||||||
offline_mode_popup=Отключить CDN даже в производственном режиме, все файлы ресурсов будут раздаваться локально.
|
offline_mode_popup=Отключить CDN даже в производственном режиме, все файлы ресурсов будут раздаваться локально.
|
||||||
disable_gravatar=Отключить службу Gravatar
|
disable_gravatar=Отключить службу Gravatar
|
||||||
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
|
disable_gravatar_popup=Отключить Gravatar и пользовательские источники, все аватары по-умолчанию загружаются пользователями.
|
||||||
disable_registration=Отключить самостоятельную регистрацию
|
disable_registration=Отключить самостоятельную регистрацию
|
||||||
disable_registration_popup=Запретить пользователям самостоятельную регистрацию, только администратор может создавать аккаунты.
|
disable_registration_popup=Запретить пользователям самостоятельную регистрацию, только администратор может создавать аккаунты.
|
||||||
enable_captcha=Включить капчу
|
enable_captcha=Включить капчу
|
||||||
|
@ -111,7 +111,7 @@ admin_title=Настройки учётной записи администра
|
||||||
admin_name=Имя пользователя
|
admin_name=Имя пользователя
|
||||||
admin_password=Пароль
|
admin_password=Пароль
|
||||||
confirm_password=Подтвердить пароль
|
confirm_password=Подтвердить пароль
|
||||||
admin_email=Эл. почта
|
admin_email=Электронная почта администратора
|
||||||
install_gogs=Установить Gogs
|
install_gogs=Установить Gogs
|
||||||
test_git_failed=Не удалось проверить 'git' команду: %v
|
test_git_failed=Не удалось проверить 'git' команду: %v
|
||||||
sqlite3_not_available=Ваша версия не поддерживает SQLite3, пожалуйста скачайте официальную бинарную версию от %s, а не версию gobuild.
|
sqlite3_not_available=Ваша версия не поддерживает SQLite3, пожалуйста скачайте официальную бинарную версию от %s, а не версию gobuild.
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=Забыли пароль
|
||||||
forget_password=Забыли пароль?
|
forget_password=Забыли пароль?
|
||||||
sign_up_now=Нужен аккаунт? Зарегистрируйтесь.
|
sign_up_now=Нужен аккаунт? Зарегистрируйтесь.
|
||||||
confirmation_mail_sent_prompt=Новое письмо для подтверждения было направлено на <b>%s</b>, пожалуйста, проверьте ваш почтовый ящик в течение %d часов для завершения регистрации.
|
confirmation_mail_sent_prompt=Новое письмо для подтверждения было направлено на <b>%s</b>, пожалуйста, проверьте ваш почтовый ящик в течение %d часов для завершения регистрации.
|
||||||
sign_in_to_account=Войдите свой аккаунт
|
|
||||||
active_your_account=Активируйте свой аккаунт
|
active_your_account=Активируйте свой аккаунт
|
||||||
resent_limit_prompt=Вы слишком часто отправляете письмо с активацией. Подождите 3 минуты, пожалуйста.
|
resent_limit_prompt=Вы слишком часто отправляете письмо с активацией. Подождите 3 минуты, пожалуйста.
|
||||||
has_unconfirmed_mail=Здравствуйте, %s! У вас есть неподтвержденный адрес электронной почты (<b>%s</b>). Если вам не приходило письмо с подтверждением или нужно выслать новое письмо, нажмите на кнопку ниже.
|
has_unconfirmed_mail=Здравствуйте, %s! У вас есть неподтвержденный адрес электронной почты (<b>%s</b>). Если вам не приходило письмо с подтверждением или нужно выслать новое письмо, нажмите на кнопку ниже.
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Пожалуйста активируйте свой аккау
|
||||||
activate_email=Подтвердите адрес своей электронной почты
|
activate_email=Подтвердите адрес своей электронной почты
|
||||||
reset_password=Восстановите ваш пароль
|
reset_password=Восстановите ваш пароль
|
||||||
register_success=Регистрация окончена. Добро пожаловать!
|
register_success=Регистрация окончена. Добро пожаловать!
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=Да
|
yes=Да
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=«должен содержать по крайней мере %s
|
||||||
max_size_error=` должен содержать максимум %s символов.`
|
max_size_error=` должен содержать максимум %s символов.`
|
||||||
email_error=«не является адресом электронной почты.»
|
email_error=«не является адресом электронной почты.»
|
||||||
url_error=«не является допустимым URL-адресом.»
|
url_error=«не является допустимым URL-адресом.»
|
||||||
|
include_error=` должен содержать '%s'`
|
||||||
unknown_error=Неизвестная ошибка:
|
unknown_error=Неизвестная ошибка:
|
||||||
captcha_incorrect=CAPTCHA не совпадает.
|
captcha_incorrect=CAPTCHA не совпадает.
|
||||||
password_not_match=Пароль и подтверждение пароля не совпадают.
|
password_not_match=Пароль и подтверждение пароля не совпадают.
|
||||||
|
@ -318,9 +319,9 @@ token_name=Имя маркера
|
||||||
generate_token=Генерировать маркер
|
generate_token=Генерировать маркер
|
||||||
generate_token_succees=Успешно создан новый токен доступа! Пожалуйста сделайте копию вашего нового токена персонального доступа. Вы не сможете увидеть его снова!
|
generate_token_succees=Успешно создан новый токен доступа! Пожалуйста сделайте копию вашего нового токена персонального доступа. Вы не сможете увидеть его снова!
|
||||||
delete_token=Удалить
|
delete_token=Удалить
|
||||||
access_token_deletion=Personal Access Token Deletion
|
access_token_deletion=Удаление персонального токена доступа
|
||||||
access_token_deletion_desc=Delete this personal access token will remove all related accesses of application. Do you want to continue?
|
access_token_deletion_desc=Удаление этого персонального токена доступа приведет к удалению всех связанных прав доступа к приложению. Вы хотите продолжить?
|
||||||
delete_token_success=Personal access token has been removed successfully! Don't forget to update your application as well.
|
delete_token_success=Персональный токен доступа успешно удален! Не забудьте изменить настройки вашего приложения.
|
||||||
|
|
||||||
delete_account=Удалить свой аккаунт
|
delete_account=Удалить свой аккаунт
|
||||||
delete_prompt=Этим действием вы удалите свою учетную запись навсегда и <strong>НЕ СМОЖЕТЕ</strong> ее вернуть!
|
delete_prompt=Этим действием вы удалите свою учетную запись навсегда и <strong>НЕ СМОЖЕТЕ</strong> ее вернуть!
|
||||||
|
@ -333,9 +334,10 @@ owner=Владелец
|
||||||
repo_name=Имя репозитория
|
repo_name=Имя репозитория
|
||||||
repo_name_helper=Лучшие названия репозиториев коротки, запоминаемы и <strong>уникальны</strong>.
|
repo_name_helper=Лучшие названия репозиториев коротки, запоминаемы и <strong>уникальны</strong>.
|
||||||
visibility=Видимость
|
visibility=Видимость
|
||||||
visiblity_helper=This repository is <span class="ui red text">Private</span>
|
visiblity_helper=<span class="ui red text">Личный</span> репозиторий
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=Все новые репозитории являются <span class="ui red text">Личными</span> по желанию администратора сайта
|
||||||
visiblity_fork_helper=(Change of this value will affect all forks)
|
visiblity_fork_helper=(Изменение этого значения затронет все форки)
|
||||||
|
clone_helper=Нужна помощь в клонировании? Посетите страницу <a target="_blank" href="%s">помощи</a>!
|
||||||
fork_repo=Ответвить репозиторий
|
fork_repo=Ответвить репозиторий
|
||||||
fork_from=Ответвление от
|
fork_from=Ответвление от
|
||||||
fork_visiblity_helper=Ответвленному репозиторию нельзя поменять уровень видимости
|
fork_visiblity_helper=Ответвленному репозиторию нельзя поменять уровень видимости
|
||||||
|
@ -350,26 +352,29 @@ auto_init=Инициализировать этот репозиторий вы
|
||||||
create_repo=Создать репозиторий
|
create_repo=Создать репозиторий
|
||||||
default_branch=Ветка по умолчанию
|
default_branch=Ветка по умолчанию
|
||||||
mirror_interval=Интервал зеркалирования (час)
|
mirror_interval=Интервал зеркалирования (час)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=Имя репозитория '%s' зарезервировано.
|
form.name_reserved=Имя репозитория '%s' зарезервировано.
|
||||||
form.name_pattern_not_allowed=Шаблон имени репозитория '%s' не допускается.
|
form.name_pattern_not_allowed=Шаблон имени репозитория '%s' не допускается.
|
||||||
|
|
||||||
need_auth=Требуется авторизация
|
need_auth=Требуется авторизация
|
||||||
migrate_type=Тип миграции
|
migrate_type=Тип миграции
|
||||||
migrate_type_helper=This repository will be a <span class="text blue">mirror</span>
|
migrate_type_helper=Этот репозиторий будет <span class="text blue">зеркалом</span>
|
||||||
migrate_repo=Перенос репозитория
|
migrate_repo=Перенос репозитория
|
||||||
migrate.clone_address=Скопировать адрес
|
migrate.clone_address=Скопировать адрес
|
||||||
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
|
migrate.clone_address_desc=Это может быть HTTP/HTTPS/GIT адрес или локальный путь на сервере.
|
||||||
|
migrate.permission_denied=У вас нет прав на импорт локальных репозиториев.
|
||||||
migrate.invalid_local_path=Недопустимый локальный путь. Возможно он не существует или является не папкой.
|
migrate.invalid_local_path=Недопустимый локальный путь. Возможно он не существует или является не папкой.
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=forked from
|
forked_from=форк от
|
||||||
fork_from_self=Вы не можете форкнуть репозитарий, так как Вы уже его владелец!
|
fork_from_self=Вы не можете форкнуть репозитарий, так как Вы уже его владелец!
|
||||||
copy_link=Скопировать
|
copy_link=Скопировать
|
||||||
copy_link_success=Скопировано!
|
copy_link_success=Скопировано!
|
||||||
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
copy_link_error=Нажмите ⌘-C или Ctrl-C для копирования
|
||||||
click_to_copy=Скопировать в буфер обмена
|
|
||||||
copied=Успешно скопировано
|
copied=Успешно скопировано
|
||||||
clone_helper=Нужна помощь в клонировании? Посетите страницу <a target="_blank" href="%s">помощи</a>!
|
|
||||||
unwatch=Перестать следить
|
unwatch=Перестать следить
|
||||||
watch=Следить
|
watch=Следить
|
||||||
unstar=Убрать из избранного
|
unstar=Убрать из избранного
|
||||||
|
@ -381,16 +386,15 @@ quick_guide=Краткое руководство
|
||||||
clone_this_repo=Клонировать репозиторий
|
clone_this_repo=Клонировать репозиторий
|
||||||
create_new_repo_command=Создать новый репозиторий из командной строки
|
create_new_repo_command=Создать новый репозиторий из командной строки
|
||||||
push_exist_repo=Отправить существующий репозиторий из командной строки
|
push_exist_repo=Отправить существующий репозиторий из командной строки
|
||||||
repo_is_empty=This repository is empty, please come back later!
|
repo_is_empty=Этот репозиторий пуст, пожалуйста, возвращайтесь позже!
|
||||||
|
|
||||||
|
|
||||||
branch=Ветка
|
branch=Ветка
|
||||||
tree=Дерево
|
tree=Дерево
|
||||||
branch_and_tags=Ветки и метки
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=Ветки
|
branches=Ветки
|
||||||
tags=Метки
|
tags=Метки
|
||||||
issues=Обсуждения
|
issues=Обсуждения
|
||||||
pulls=Pull Requests
|
pulls=Пулл реквесты
|
||||||
labels=Метки
|
labels=Метки
|
||||||
milestones=Этапы
|
milestones=Этапы
|
||||||
commits=Коммиты
|
commits=Коммиты
|
||||||
|
@ -413,26 +417,26 @@ issues.new=Новая задача
|
||||||
issues.new.labels=Метки
|
issues.new.labels=Метки
|
||||||
issues.new.no_label=Не метка
|
issues.new.no_label=Не метка
|
||||||
issues.new.clear_labels=Отчистить метки
|
issues.new.clear_labels=Отчистить метки
|
||||||
issues.new.milestone=Milestone
|
issues.new.milestone=Этап
|
||||||
issues.new.no_milestone=No Milestone
|
issues.new.no_milestone=Нет этапа
|
||||||
issues.new.clear_milestone=Clear milestone
|
issues.new.clear_milestone=Очистить этап
|
||||||
issues.new.open_milestone=Open Milestones
|
issues.new.open_milestone=Открыть этап
|
||||||
issues.new.closed_milestone=Closed Milestones
|
issues.new.closed_milestone=Завершенные этапы
|
||||||
issues.new.assignee=Assignee
|
issues.new.assignee=Ответственный
|
||||||
issues.new.clear_assignee=Clear assignee
|
issues.new.clear_assignee=Убрать ответственного
|
||||||
issues.new.no_assignee=No assignee
|
issues.new.no_assignee=Нет ответственного
|
||||||
issues.create=Create Issue
|
issues.create=Добавить задачу
|
||||||
issues.new_label=Новая метка
|
issues.new_label=Новая метка
|
||||||
issues.new_label_placeholder=Имя метки...
|
issues.new_label_placeholder=Имя метки...
|
||||||
issues.create_label=Create Label
|
issues.create_label=Добавить метку
|
||||||
issues.open_tab=%d Открыть
|
issues.open_tab=%d Открыть
|
||||||
issues.close_tab=%d Закрыть
|
issues.close_tab=%d Закрыть
|
||||||
issues.filter_label=Метка
|
issues.filter_label=Метка
|
||||||
issues.filter_label_no_select=Нет выбранной метки
|
issues.filter_label_no_select=Нет выбранной метки
|
||||||
issues.filter_milestone=Этап
|
issues.filter_milestone=Этап
|
||||||
issues.filter_milestone_no_select=No selected milestone
|
issues.filter_milestone_no_select=Этап не выбран
|
||||||
issues.filter_assignee=Назначено
|
issues.filter_assignee=Назначено
|
||||||
issues.filter_assginee_no_select=No selected Assignee
|
issues.filter_assginee_no_select=Ответственный не выбран
|
||||||
issues.filter_type=Тип
|
issues.filter_type=Тип
|
||||||
issues.filter_type.all_issues=Все задачи
|
issues.filter_type.all_issues=Все задачи
|
||||||
issues.filter_type.assigned_to_you=Назначено Вам
|
issues.filter_type.assigned_to_you=Назначено Вам
|
||||||
|
@ -441,35 +445,35 @@ issues.filter_type.mentioning_you=Вы упомянуты
|
||||||
issues.filter_sort=Сортировать
|
issues.filter_sort=Сортировать
|
||||||
issues.filter_sort.latest=Новейшие
|
issues.filter_sort.latest=Новейшие
|
||||||
issues.filter_sort.oldest=Старейшие
|
issues.filter_sort.oldest=Старейшие
|
||||||
issues.filter_sort.recentupdate=Recently updated
|
issues.filter_sort.recentupdate=Недавно обновленные
|
||||||
issues.filter_sort.leastupdate=Least recently updated
|
issues.filter_sort.leastupdate=Давно обновленные
|
||||||
issues.filter_sort.mostcomment=Most commented
|
issues.filter_sort.mostcomment=Большего комментариев
|
||||||
issues.filter_sort.leastcomment=Least commented
|
issues.filter_sort.leastcomment=Меньше комментариев
|
||||||
issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a>
|
issues.opened_by=%[1] открыта <a href="%[2]s">%[3]s</a>
|
||||||
issues.opened_by_fake=opened %[1]s by %[2]s
|
issues.opened_by_fake=%[1]s открыта %[2]s
|
||||||
issues.previous=Предыдущая страница
|
issues.previous=Предыдущая страница
|
||||||
issues.next=Следующая страница
|
issues.next=Следующая страница
|
||||||
issues.open_title=Open
|
issues.open_title=Открыта
|
||||||
issues.closed_title=Closed
|
issues.closed_title=Закрыта
|
||||||
issues.num_comments=%d comments
|
issues.num_comments=комментариев: %d
|
||||||
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commented_at=` прокомментировал <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.no_content=There is no content yet.
|
issues.no_content=Пока нет содержимого.
|
||||||
issues.close_issue=Закрыть
|
issues.close_issue=Закрыть
|
||||||
issues.close_comment_issue=Закрыть и комментировать
|
issues.close_comment_issue=Прокомментировать и закрыть
|
||||||
issues.reopen_issue=Reopen
|
issues.reopen_issue=Открыть снова
|
||||||
issues.reopen_comment_issue=Reopen and comment
|
issues.reopen_comment_issue=Прокомментировать и открыть
|
||||||
issues.create_comment=Комментировать
|
issues.create_comment=Комментировать
|
||||||
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at=`закрыл <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at=`reopened <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=`referenced this issue from a commit <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=Poster
|
issues.poster=Автор
|
||||||
issues.admin=Администратор
|
issues.admin=Администратор
|
||||||
issues.owner=Владелец
|
issues.owner=Владелец
|
||||||
issues.sign_up_for_free=Зарегистрируйтесь бесплатно
|
issues.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.sign_in_require_desc=чтобы присоединиться к обсуждению. Уже есть аккаунт? <a href="%s">Войдите чтобы прокомментировать</a>
|
||||||
issues.edit=Изменить
|
issues.edit=Изменить
|
||||||
issues.cancel=Отмена
|
issues.cancel=Отмена
|
||||||
issues.save=Save
|
issues.save=Сохранить
|
||||||
issues.label_title=Имя метки
|
issues.label_title=Имя метки
|
||||||
issues.label_color=Цвет метки
|
issues.label_color=Цвет метки
|
||||||
issues.label_count=%d меток
|
issues.label_count=%d меток
|
||||||
|
@ -481,54 +485,55 @@ issues.label_deletion=Удаление метки
|
||||||
issues.label_deletion_desc=Удаление ярлыка затронет все связанные задачи. Продолжить?
|
issues.label_deletion_desc=Удаление ярлыка затронет все связанные задачи. Продолжить?
|
||||||
issues.label_deletion_success=Метка была удалена успешно!
|
issues.label_deletion_success=Метка была удалена успешно!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=Сравнить изменения
|
pulls.compare_changes=Сравнить изменения
|
||||||
pulls.compare_changes_desc=Compare two branches and make a pull request for changes.
|
pulls.compare_changes_desc=Сравнить две ветки и создать пулл реквест для изменений.
|
||||||
pulls.compare_base=base
|
pulls.compare_base=родительская ветка
|
||||||
pulls.compare_compare=compare
|
pulls.compare_compare=сравнить
|
||||||
pulls.filter_branch=Filter branch
|
pulls.filter_branch=Фильтр по ветке
|
||||||
pulls.no_results=No results found.
|
pulls.no_results=Результатов не найдено.
|
||||||
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even.
|
pulls.nothing_to_compare=Нечего сравнивать, родительская и текущая ветка одинаковые.
|
||||||
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.has_pull_request=`Уже существует пулл-реквест между двумя целями <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
|
||||||
pulls.create=Create Pull Request
|
pulls.create=Создать пулл-реквест
|
||||||
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code>
|
pulls.title_desc=хочет смерджить %[1]d коммит(ов) из <code>%[2]s</code> в <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.merged_title_desc=слито %[1]d коммит(ов) из <code>%[2]s</code> в <code>%[3]s</code> %[4]s
|
||||||
pulls.tab_conversation=Обсуждение
|
pulls.tab_conversation=Обсуждение
|
||||||
pulls.tab_commits=Коммиты
|
pulls.tab_commits=Коммиты
|
||||||
pulls.tab_files=Измененные файлы
|
pulls.tab_files=Измененные файлы
|
||||||
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
|
pulls.reopen_to_merge=Пожалуйста пересоздайте пулл-реквест для слияния.
|
||||||
pulls.merged=Merged
|
pulls.merged=Слито
|
||||||
pulls.has_merged=This pull request has been merged successfully!
|
pulls.has_merged=Слияние этого пулл-реквеста успешно завершено!
|
||||||
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
|
pulls.data_broken=Содержимое этого пулл-реквеста было нарушено, вследствии удаления или клонирования информации.
|
||||||
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
|
pulls.is_checking=Продолжается проверка конфликтов, пожалуйста обновите страницу несколько позже.
|
||||||
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
|
pulls.can_auto_merge_desc=Вы можете провести операцию автоматического слияния для этого пулл-реквеста.
|
||||||
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits.
|
pulls.cannot_auto_merge_desc=Вы не можете произвести операцию автоматического слияния, потому как существуют конфликты между коммитами.
|
||||||
pulls.cannot_auto_merge_helper=Please use command line tool to solve it.
|
pulls.cannot_auto_merge_helper=Используйте командную строку для решения этого.
|
||||||
pulls.merge_pull_request=Merge Pull Request
|
pulls.merge_pull_request=Слить пулл-реквест
|
||||||
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
|
pulls.open_unmerged_pull_exists=`Вы не можете произвести операцию переоткрытия, потому что уже существует пулл-реквест (#%d) из этого же репозитория, с такими же параметрами слияния, который ожидает слияния.`
|
||||||
|
|
||||||
milestones.new=New Milestone
|
milestones.new=Новая контрольная точка
|
||||||
milestones.open_tab=%d Open
|
milestones.open_tab=%d открыты
|
||||||
milestones.close_tab=%d Closed
|
milestones.close_tab=%d Закрыт
|
||||||
milestones.closed=Closed %s
|
milestones.closed=Закрыт %s
|
||||||
milestones.no_due_date=No due date
|
milestones.no_due_date=Срок не указан
|
||||||
milestones.open=Open
|
milestones.open=Открыть
|
||||||
milestones.close=Close
|
milestones.close=Закрыть
|
||||||
milestones.new_subheader=Create milestones to organize your issues.
|
milestones.new_subheader=Создавайте контрольные точки для трекинга ваших вопросов.
|
||||||
milestones.create=Create Milestone
|
milestones.create=Создать контрольную точку
|
||||||
milestones.title=Title
|
milestones.title=Заголовок
|
||||||
milestones.desc=Description
|
milestones.desc=Описание
|
||||||
milestones.due_date=Due Date (optional)
|
milestones.due_date=Дата окончания (опционально)
|
||||||
milestones.clear=Clear
|
milestones.clear=Очистить
|
||||||
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'.
|
milestones.invalid_due_date_format=Некорректная дата окончания. Правильный формат - 'гггг-мм-дд'.
|
||||||
milestones.create_success=Milestone '%s' has been created successfully!
|
milestones.create_success=Контрольная точка '%s' успешно создана!
|
||||||
milestones.edit=Edit Milestone
|
milestones.edit=Изменить контрольную точку
|
||||||
milestones.edit_subheader=Use better description for milestones so people won't be confused.
|
milestones.edit_subheader=Используйте лучшее описание контрольной точки, во избежание непонимания со стороны других людей.
|
||||||
milestones.cancel=Cancel
|
milestones.cancel=Отмена
|
||||||
milestones.modify=Modify Milestone
|
milestones.modify=Изменить контрольную точку
|
||||||
milestones.edit_success=Changes of milestone '%s' has been saved successfully!
|
milestones.edit_success=Изменения контрольной точки '%s' успешно сохранены!
|
||||||
milestones.deletion=Milestone Deletion
|
milestones.deletion=Удаление контрольной точки
|
||||||
milestones.deletion_desc=Delete this milestone will remove its information in all related issues. Do you want to continue?
|
milestones.deletion_desc=Удаление этой контрольной точки приведет с удалению всей информации, во всех вопросах (Issues). Вы действительно хотите продолжить?
|
||||||
milestones.deletion_success=Milestone has been deleted successfully!
|
milestones.deletion_success=Контрольная точка успешно удалена!
|
||||||
|
|
||||||
settings=Настройки
|
settings=Настройки
|
||||||
settings.options=Опции
|
settings.options=Опции
|
||||||
|
@ -539,20 +544,20 @@ settings.basic_settings=Основные параметры
|
||||||
settings.danger_zone=Опасная зона
|
settings.danger_zone=Опасная зона
|
||||||
settings.site=Официальный сайт
|
settings.site=Официальный сайт
|
||||||
settings.update_settings=Обновить настройки
|
settings.update_settings=Обновить настройки
|
||||||
settings.change_reponame_prompt=This change will affect how links relate to the repository.
|
settings.change_reponame_prompt=Это изменение повлияет на отношения ссылок к этому репозиторию.
|
||||||
settings.transfer=Передать права собственности
|
settings.transfer=Передать права собственности
|
||||||
settings.transfer_desc=Передать репозиторий другому пользователю или организации где у вас есть права администратора.
|
settings.transfer_desc=Передать репозиторий другому пользователю или организации где у вас есть права администратора.
|
||||||
settings.new_owner_has_same_repo=У нового владельца уже есть хранилище с таким названием.
|
settings.new_owner_has_same_repo=У нового владельца уже есть хранилище с таким названием.
|
||||||
settings.delete=Удалить этот репозиторий
|
settings.delete=Удалить этот репозиторий
|
||||||
settings.delete_desc=Как только вы удалите репозиторий — пути назад не будет. Удостоверьтесь, что вам это точно нужно.
|
settings.delete_desc=Как только вы удалите репозиторий — пути назад не будет. Удостоверьтесь, что вам это точно нужно.
|
||||||
settings.transfer_notices_1=- You will lose access if new owner is a individual user.
|
settings.transfer_notices_1=- Вы можете потерять доступ, если новый владелец является отдельным пользователем.
|
||||||
settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners.
|
settings.transfer_notices_2=- Вы сохраните доступ, если новым владельцем станет организация, владельцем которой вы являетесь.
|
||||||
settings.transfer_form_title=Please enter following information to confirm your operation:
|
settings.transfer_form_title=Введите сопутствующую информацию для подтверждения операции:
|
||||||
settings.delete_notices_1=- This operation <strong>CANNOT</strong> be undone.
|
settings.delete_notices_1=- Эта операция <strong>НЕ МОЖЕТ</strong> быть отменена.
|
||||||
settings.delete_notices_2=- This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators.
|
settings.delete_notices_2=- Эта операция перманентно удалит всё из этого репозитория, включая данные Git, связанные с ним вопросы, комментарии и права доступа для сотрудников.
|
||||||
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion.
|
settings.delete_notices_fork_1=- Если данный репозиторий является публичным, все склонированные репозитории останутся независимыми, после его удаления.
|
||||||
settings.delete_notices_fork_2=- If this repository is private, all forks will be removed at the same time.
|
settings.delete_notices_fork_2=- Если данный репозиторий является приватным, все его форки будут удалены вместе с ним.
|
||||||
settings.delete_notices_fork_3=- If you want to keep all forks after deletion, please change visibility of this repository to public first.
|
settings.delete_notices_fork_3=- Если вы хотите сохранить все форки после удаления репозитория, то сначала сделайте его публичным.
|
||||||
settings.update_settings_success=Настройка репозитория обновлена успешно.
|
settings.update_settings_success=Настройка репозитория обновлена успешно.
|
||||||
settings.transfer_owner=Новый владелец
|
settings.transfer_owner=Новый владелец
|
||||||
settings.make_transfer=Выполнить передачу
|
settings.make_transfer=Выполнить передачу
|
||||||
|
@ -561,19 +566,20 @@ settings.confirm_delete=Подтвердить удаление
|
||||||
settings.add_collaborator=Добавить нового соавтора
|
settings.add_collaborator=Добавить нового соавтора
|
||||||
settings.add_collaborator_success=Был добавлен новый соавтор.
|
settings.add_collaborator_success=Был добавлен новый соавтор.
|
||||||
settings.remove_collaborator_success=Соавтор был удален.
|
settings.remove_collaborator_success=Соавтор был удален.
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=Пользователь является членом организации, члены которой не могут быть добавлены в качестве соавтора.
|
settings.user_is_org_member=Пользователь является членом организации, члены которой не могут быть добавлены в качестве соавтора.
|
||||||
settings.add_webhook=Добавить Webhook
|
settings.add_webhook=Добавить Webhook
|
||||||
settings.hooks_desc=Webhooks позволяют внешним службам получать уведомления при возникновении определенных событий на Gogs. При возникновении указанных событий мы отправим запрос POST на каждый заданный вами URL. Узнать больше можно в нашем <a target="_blank" href="%s">Руководстве по Webhooks</a>.
|
settings.hooks_desc=Webhooks позволяют внешним службам получать уведомления при возникновении определенных событий на Gogs. При возникновении указанных событий мы отправим запрос POST на каждый заданный вами URL. Узнать больше можно в нашем <a target="_blank" href="%s">Руководстве по Webhooks</a>.
|
||||||
settings.webhook_deletion=Delete Webhook
|
settings.webhook_deletion=Удалить веб-хук
|
||||||
settings.webhook_deletion_desc=Delete this webhook will remove its information and all delivery history. Do you want to continue?
|
settings.webhook_deletion_desc=Удаление этого веб-хука приведет к удалению всей, связанной с ним, информации, включая историю. Хотите продолжить?
|
||||||
settings.webhook_deletion_success=Webhook has been deleted successfully!
|
settings.webhook_deletion_success=Веб-хук успешно удален!
|
||||||
settings.webhook.request=Request
|
settings.webhook.request=Запрос
|
||||||
settings.webhook.response=Response
|
settings.webhook.response=Ответ
|
||||||
settings.webhook.headers=Headers
|
settings.webhook.headers=Заголовки
|
||||||
settings.webhook.payload=Payload
|
settings.webhook.payload=Содержимое запроса
|
||||||
settings.webhook.body=Body
|
settings.webhook.body=Тело ответа
|
||||||
settings.githooks_desc=Git Hooks are powered by Git itself, you can edit files of supported hooks in the list below to perform custom operations.
|
settings.githooks_desc=Git-хуки предоставляются Git самим по себе, вы можете изменять файлы поддерживаемых хуков из списка ниже чтобы выполнять внешние операции.
|
||||||
settings.githook_edit_desc=If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.
|
settings.githook_edit_desc=Если хук не активен, будет подставлен пример содержимого. Пустое значение в этом поле приведет к отключению хука.
|
||||||
settings.githook_name=Название Hook'a
|
settings.githook_name=Название Hook'a
|
||||||
settings.githook_content=Перехватить содержание
|
settings.githook_content=Перехватить содержание
|
||||||
settings.update_githook=Обновить Hook
|
settings.update_githook=Обновить Hook
|
||||||
|
@ -581,19 +587,19 @@ settings.add_webhook_desc=Мы отправим запрос <code>POST</code>
|
||||||
settings.payload_url=URL обработчика
|
settings.payload_url=URL обработчика
|
||||||
settings.content_type=Тип содержимого
|
settings.content_type=Тип содержимого
|
||||||
settings.secret=Secret
|
settings.secret=Secret
|
||||||
settings.slack_username=Username
|
settings.slack_username=Имя пользователя
|
||||||
settings.slack_icon_url=Icon URL
|
settings.slack_icon_url=URL иконки
|
||||||
settings.slack_color=Color
|
settings.slack_color=Цвет
|
||||||
settings.event_desc=На какие события этот webhook должен срабатывать?
|
settings.event_desc=На какие события этот webhook должен срабатывать?
|
||||||
settings.event_push_only=Просто <code>push</code> событие.
|
settings.event_push_only=Просто <code>push</code> событие.
|
||||||
settings.event_send_everything=I need <strong>everything</strong>.
|
settings.event_send_everything=Мне нужно <strong>все</strong>.
|
||||||
settings.event_choose=Let me choose what I need.
|
settings.event_choose=Позвольте мне выбрать то, что нужно.
|
||||||
settings.event_create=Create
|
settings.event_create=Создать
|
||||||
settings.event_create_desc=Branch, or tag created
|
settings.event_create_desc=Ветка или тэг созданы
|
||||||
settings.event_push=Push
|
settings.event_push=Push
|
||||||
settings.event_push_desc=Git push to a repository
|
settings.event_push_desc=Push в репозиторий
|
||||||
settings.active=Активен
|
settings.active=Активен
|
||||||
settings.active_helper=Details regarding the event which triggered the hook will be delivered as well.
|
settings.active_helper=Подробности о событии, вызвавшем срабатывание хука, также будут предоставлены.
|
||||||
settings.add_hook_success=Был добавлен новый webhook.
|
settings.add_hook_success=Был добавлен новый webhook.
|
||||||
settings.update_webhook=Обновление Webhook
|
settings.update_webhook=Обновление Webhook
|
||||||
settings.update_hook_success=Webhook обновлен.
|
settings.update_hook_success=Webhook обновлен.
|
||||||
|
@ -605,16 +611,16 @@ settings.slack_token=Token
|
||||||
settings.slack_domain=Домен
|
settings.slack_domain=Домен
|
||||||
settings.slack_channel=Канал
|
settings.slack_channel=Канал
|
||||||
settings.deploy_keys=Ключи развертывания
|
settings.deploy_keys=Ключи развертывания
|
||||||
settings.add_deploy_key=Add Deploy Key
|
settings.add_deploy_key=Добавить ключ развертывания
|
||||||
settings.no_deploy_keys=You haven't added any deploy key.
|
settings.no_deploy_keys=Вы не добавляли ключи развертывания.
|
||||||
settings.title=Title
|
settings.title=Заголовок
|
||||||
settings.deploy_key_content=Content
|
settings.deploy_key_content=Содержимое
|
||||||
settings.key_been_used=Deploy key content has been used.
|
settings.key_been_used=Содержимое ключа развертывания уже используется.
|
||||||
settings.key_name_used=Deploy key with same name has already existed.
|
settings.key_name_used=Ключ развертывания с таким заголовком уже существует.
|
||||||
settings.add_key_success=New deploy key '%s' has been added successfully!
|
settings.add_key_success=Новый ключ развертывания '%s' успешно добавлен!
|
||||||
settings.deploy_key_deletion=Delete Deploy Key
|
settings.deploy_key_deletion=Удалить ключ развертывания
|
||||||
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_desc=Удаление ключа развертывания приведет к удалению всех связанных прав доступа к репозиторию. Вы хотите продолжить?
|
||||||
settings.deploy_key_deletion_success=Deploy key has been deleted successfully!
|
settings.deploy_key_deletion_success=Ключ развертывания успешно удален!
|
||||||
|
|
||||||
diff.browse_source=Просмотр исходного кода
|
diff.browse_source=Просмотр исходного кода
|
||||||
diff.parent=Родитель
|
diff.parent=Родитель
|
||||||
|
@ -633,24 +639,32 @@ release.stable=Стабильный
|
||||||
release.edit=Редактировать
|
release.edit=Редактировать
|
||||||
release.ahead=<strong>%d</strong> коммитов %s начиная с этого релиза
|
release.ahead=<strong>%d</strong> коммитов %s начиная с этого релиза
|
||||||
release.source_code=Исходный код
|
release.source_code=Исходный код
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=Имя тега
|
release.tag_name=Имя тега
|
||||||
release.target=Цель
|
release.target=Цель
|
||||||
release.tag_helper=Выберите существующий тег, или создайте новый.
|
release.tag_helper=Выберите существующий тег, или создайте новый.
|
||||||
release.release_title=Название релиза
|
release.title=Title
|
||||||
release.content_with_md=Содержимое с <a href="%s">Markdown</a>
|
release.content=Content
|
||||||
release.write=Запись
|
release.write=Запись
|
||||||
release.preview=Предварительный просмотр
|
release.preview=Предварительный просмотр
|
||||||
release.content_placeholder=Напишите что-нибудь
|
|
||||||
release.loading=Загрузка...
|
release.loading=Загрузка...
|
||||||
release.prerelease_desc=Это предварительный релиз
|
release.prerelease_desc=Это предварительный релиз
|
||||||
release.prerelease_helper=We’ll point out that this release is not production-ready.
|
release.prerelease_helper=Отдельно отметим, что этот релиз не готов к использованию в продакшене.
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=Опубликовать релиз
|
release.publish=Опубликовать релиз
|
||||||
release.save_draft=Сохранить черновик
|
release.save_draft=Сохранить черновик
|
||||||
release.edit_release=Редактировать релиз
|
release.edit_release=Редактировать релиз
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=Релиз с этим именем тега уже существует.
|
release.tag_name_already_exist=Релиз с этим именем тега уже существует.
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Название организации
|
org_name_holder=Название организации
|
||||||
|
org_full_name_holder=Полное название организации
|
||||||
org_name_helper=Лучшие названия организаций коротки и запоминаемы.
|
org_name_helper=Лучшие названия организаций коротки и запоминаемы.
|
||||||
create_org=Создать Организацию
|
create_org=Создать Организацию
|
||||||
repo_updated=Обновлено
|
repo_updated=Обновлено
|
||||||
|
@ -664,7 +678,7 @@ org_desc=Описание
|
||||||
team_name=Название команды
|
team_name=Название команды
|
||||||
team_desc=Описание
|
team_desc=Описание
|
||||||
team_name_helper=Вы будете использовать это имя для упоминания этой команды в обсуждении.
|
team_name_helper=Вы будете использовать это имя для упоминания этой команды в обсуждении.
|
||||||
team_desc_helper=What is this team all about?
|
team_desc_helper=Что это за команда?
|
||||||
team_permission_desc=Какой уровень разрешений должен быть у этой команды?
|
team_permission_desc=Какой уровень разрешений должен быть у этой команды?
|
||||||
|
|
||||||
form.name_reserved=Наименование организации '%s' зарезервированно.
|
form.name_reserved=Наименование организации '%s' зарезервированно.
|
||||||
|
@ -677,8 +691,8 @@ settings.website=Сайт
|
||||||
settings.location=Местоположение
|
settings.location=Местоположение
|
||||||
settings.update_settings=Обновить настройки
|
settings.update_settings=Обновить настройки
|
||||||
settings.update_setting_success=Настройки Организации были успешно обновлены.
|
settings.update_setting_success=Настройки Организации были успешно обновлены.
|
||||||
settings.change_orgname_prompt=This change will affect how links relate to the organization.
|
settings.change_orgname_prompt=Это изменение затронет все связанные с организацией, ссылки.
|
||||||
settings.update_avatar_success=Organization avatar setting has been updated successfully.
|
settings.update_avatar_success=Аватар организации успешно обновлен.
|
||||||
settings.delete=Удалить Организацию
|
settings.delete=Удалить Организацию
|
||||||
settings.delete_account=Удалить Эту Организацию
|
settings.delete_account=Удалить Эту Организацию
|
||||||
settings.delete_prompt=Это действие безвозвратно удалит эту организацию навсегда.
|
settings.delete_prompt=Это действие безвозвратно удалит эту организацию навсегда.
|
||||||
|
@ -719,7 +733,7 @@ teams.delete_team_desc=Эта команда будет удалена. Вы х
|
||||||
teams.delete_team_success=Данная команда была удалена успешно.
|
teams.delete_team_success=Данная команда была удалена успешно.
|
||||||
teams.read_permission_desc=Эта команда предоставляет доступ на <strong>Чтение</strong>: члены могут просматривать и клонировать репозитории команды.
|
teams.read_permission_desc=Эта команда предоставляет доступ на <strong>Чтение</strong>: члены могут просматривать и клонировать репозитории команды.
|
||||||
teams.write_permission_desc=Эта команда предоставляет доступ на <strong>Запись</strong>: члены могут получать и выполнять push команды в репозитории.
|
teams.write_permission_desc=Эта команда предоставляет доступ на <strong>Запись</strong>: члены могут получать и выполнять push команды в репозитории.
|
||||||
teams.admin_permission_desc=This team grants <strong>Admin</strong> access: members can read from, push to, and add collaborators to the team's repositories.
|
teams.admin_permission_desc=Эта команда дает <strong>административный</strong> доступ: участники могут читать, пушить и добавлять соавторов к ее репозиториям.
|
||||||
teams.repositories=Репозитории группы разработки
|
teams.repositories=Репозитории группы разработки
|
||||||
teams.add_team_repository=Добавить репозиторий группы разработки
|
teams.add_team_repository=Добавить репозиторий группы разработки
|
||||||
teams.remove_repo=Удалить
|
teams.remove_repo=Удалить
|
||||||
|
@ -734,9 +748,9 @@ authentication=Авторизация
|
||||||
config=Настройки
|
config=Настройки
|
||||||
notices=Системные уведомления
|
notices=Системные уведомления
|
||||||
monitor=Мониторинг
|
monitor=Мониторинг
|
||||||
first_page=First
|
first_page=Первый
|
||||||
last_page=Last
|
last_page=Последний
|
||||||
total=Total: %d
|
total=Всего: %d
|
||||||
|
|
||||||
dashboard.statistic=Статистика
|
dashboard.statistic=Статистика
|
||||||
dashboard.operations=Операции
|
dashboard.operations=Операции
|
||||||
|
@ -751,74 +765,77 @@ dashboard.delete_inactivate_accounts=Удалить все неактивиро
|
||||||
dashboard.delete_inactivate_accounts_success=Все неактивированные учетные записи удалены успешно.
|
dashboard.delete_inactivate_accounts_success=Все неактивированные учетные записи удалены успешно.
|
||||||
dashboard.delete_repo_archives=Удаление всех архивов репозиториев
|
dashboard.delete_repo_archives=Удаление всех архивов репозиториев
|
||||||
dashboard.delete_repo_archives_success=Все архивы репозиториев были успешно удалены.
|
dashboard.delete_repo_archives_success=Все архивы репозиториев были успешно удалены.
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=Выполнить сборку мусора на репозиториях
|
dashboard.git_gc_repos=Выполнить сборку мусора на репозиториях
|
||||||
dashboard.git_gc_repos_success=Сборка мусора на всех репозиториях успешно выполнена.
|
dashboard.git_gc_repos_success=Сборка мусора на всех репозиториях успешно выполнена.
|
||||||
dashboard.resync_all_sshkeys=Переписать файл «.ssh/authorized_keys» (осторожно: не Gogs ключи будут утеряны)
|
dashboard.resync_all_sshkeys=Переписать файл «.ssh/authorized_keys» (осторожно: не Gogs ключи будут утеряны)
|
||||||
dashboard.resync_all_sshkeys_success=Были успешно переписаны все открытые ключи.
|
dashboard.resync_all_sshkeys_success=Были успешно переписаны все открытые ключи.
|
||||||
dashboard.resync_all_update_hooks=Rewrite all update hook of repositories (needed when custom config path is changed)
|
dashboard.resync_all_update_hooks=Перезаписать все апдейт-хуки этого репозитория (необходимо, когда изменен путь до папки конфигураций)
|
||||||
dashboard.resync_all_update_hooks_success=All repositories' update hook have been rewritten successfully.
|
dashboard.resync_all_update_hooks_success=Апдейт-хуки всех репозиториев успешно перезаписаны.
|
||||||
|
|
||||||
dashboard.server_uptime=Время непрерывной работы сервера
|
dashboard.server_uptime=Время непрерывной работы сервера
|
||||||
dashboard.current_goroutine=Текущий Goroutines
|
dashboard.current_goroutine=Текущий Goroutines
|
||||||
dashboard.current_memory_usage=Текущее использование памяти
|
dashboard.current_memory_usage=Текущее использование памяти
|
||||||
dashboard.total_memory_allocated=Всего памяти выделено
|
dashboard.total_memory_allocated=Всего памяти выделено
|
||||||
dashboard.memory_obtained=Memory Obtained
|
dashboard.memory_obtained=Памяти использовано
|
||||||
dashboard.pointer_lookup_times=Pointer Lookup Times
|
dashboard.pointer_lookup_times=Запросов указателя
|
||||||
dashboard.memory_allocate_times=Memory Allocate Times
|
dashboard.memory_allocate_times=Выделений памяти
|
||||||
dashboard.memory_free_times=Memory Free Times
|
dashboard.memory_free_times=Освобождений памяти
|
||||||
dashboard.current_heap_usage=Текущее использование кучи
|
dashboard.current_heap_usage=Текущее использование кучи
|
||||||
dashboard.heap_memory_obtained=Heap Memory Obtained
|
dashboard.heap_memory_obtained=Получено динамической памяти
|
||||||
dashboard.heap_memory_idle=Heap Memory Idle
|
dashboard.heap_memory_idle=Не используется динамической памяти
|
||||||
dashboard.heap_memory_in_use=Кучи памяти в работе
|
dashboard.heap_memory_in_use=Кучи памяти в работе
|
||||||
dashboard.heap_memory_released=Heap Memory Released
|
dashboard.heap_memory_released=Освобождено динамической памяти
|
||||||
dashboard.heap_objects=Heap Objects
|
dashboard.heap_objects=Объектов динамической памяти
|
||||||
dashboard.bootstrap_stack_usage=Bootstrap Stack Usage
|
dashboard.bootstrap_stack_usage=Использование стека загрузчика
|
||||||
dashboard.stack_memory_obtained=Stack Memory Obtained
|
dashboard.stack_memory_obtained=Память, занятая под стек
|
||||||
dashboard.mspan_structures_usage=MSpan Structures Usage
|
dashboard.mspan_structures_usage=Использование структур MSpan
|
||||||
dashboard.mspan_structures_obtained=MSpan Structures Obtained
|
dashboard.mspan_structures_obtained=Получено структур MSpan
|
||||||
dashboard.mcache_structures_usage=MCache Structures Usage
|
dashboard.mcache_structures_usage=Использование структур MCache
|
||||||
dashboard.mcache_structures_obtained=MCache Structures Obtained
|
dashboard.mcache_structures_obtained=Получено структур MCache
|
||||||
dashboard.profiling_bucket_hash_table_obtained=Profiling Bucket Hash Table Obtained
|
dashboard.profiling_bucket_hash_table_obtained=Хеш-таблиц получено при профилировании
|
||||||
dashboard.gc_metadata_obtained=GC Metadada Obtained
|
dashboard.gc_metadata_obtained=Получены метаданные сборщика мусора
|
||||||
dashboard.other_system_allocation_obtained=Other System Allocation Obtained
|
dashboard.other_system_allocation_obtained=Получено других системных выделений памяти
|
||||||
dashboard.next_gc_recycle=Next GC Recycle
|
dashboard.next_gc_recycle=Следующая очистка сборщика мусора
|
||||||
dashboard.last_gc_time=Since Last GC Time
|
dashboard.last_gc_time=Прошло с последнего сбора мусора
|
||||||
dashboard.total_gc_time=Total GC Pause
|
dashboard.total_gc_time=Итоговое время GC
|
||||||
dashboard.total_gc_pause=Total GC Pause
|
dashboard.total_gc_pause=Итоговая задержка GC
|
||||||
dashboard.last_gc_pause=Last GC Pause
|
dashboard.last_gc_pause=Последняя пауза сборщика мусора
|
||||||
dashboard.gc_times=GC Times
|
dashboard.gc_times=Количество сборок мусора
|
||||||
|
|
||||||
users.user_manage_panel=User Manage Panel
|
users.user_manage_panel=Панель управления пользователями
|
||||||
users.new_account=Создать новый аккаунт
|
users.new_account=Создать новый аккаунт
|
||||||
users.name=Имя
|
users.name=Имя
|
||||||
users.activated=Активирован
|
users.activated=Активирован
|
||||||
users.admin=Администратор
|
users.admin=Администратор
|
||||||
users.repos=Репозитории
|
users.repos=Репозитории
|
||||||
users.created=Создано
|
users.created=Создано
|
||||||
users.send_register_notify=Send Registration Notification To User
|
users.send_register_notify=Отправить пользователю уведомление о регистрации
|
||||||
users.new_success=New account '%s' has been created successfully.
|
users.new_success=Новая учетная запись '%s' успешно создана.
|
||||||
users.edit=Редактировать
|
users.edit=Редактировать
|
||||||
users.auth_source=Authentication Source
|
users.auth_source=Источник аутентификации
|
||||||
users.local=Локальный
|
users.local=Локальный
|
||||||
users.auth_login_name=Authentication Login Name
|
users.auth_login_name=Логин для авторизации
|
||||||
users.password_helper=Leave it empty to remain unchanged.
|
users.password_helper=Оставьте пустым, чтобы оставить без изменений.
|
||||||
users.update_profile_success=Профиль учетной записи обновлен успешно.
|
users.update_profile_success=Профиль учетной записи обновлен успешно.
|
||||||
users.edit_account=Изменение учетной записи
|
users.edit_account=Изменение учетной записи
|
||||||
users.is_activated=Эта учетная запись активирована
|
users.is_activated=Эта учетная запись активирована
|
||||||
users.is_admin=У этой учетной записи есть права администратора
|
users.is_admin=У этой учетной записи есть права администратора
|
||||||
users.allow_git_hook=Пользователь имеет право создать Git перехватчик
|
users.allow_git_hook=Пользователь имеет право создать Git перехватчик
|
||||||
|
users.allow_import_local=Пользователь имеет право импортировать локальные репозитории
|
||||||
users.update_profile=Обновить профиль учетной записи
|
users.update_profile=Обновить профиль учетной записи
|
||||||
users.delete_account=Удалить эту учетную запись
|
users.delete_account=Удалить эту учетную запись
|
||||||
users.still_own_repo=На вашем аккаунте все еще остается как минимум один репозиторий, сначала вам нужно удалить или передать его.
|
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.still_has_org=Эта учетная запись все еще является членом как минимум одной организации. Для продолжения, покиньте или удалите эту организацию.
|
||||||
users.deletion_success=Account has been deleted successfully!
|
users.deletion_success=Учетная запись успешно удалена!
|
||||||
|
|
||||||
orgs.org_manage_panel=Управление группами
|
orgs.org_manage_panel=Управление группами
|
||||||
orgs.name=Имя
|
orgs.name=Имя
|
||||||
orgs.teams=Команды
|
orgs.teams=Команды
|
||||||
orgs.members=Участники
|
orgs.members=Участники
|
||||||
|
|
||||||
repos.repo_manage_panel=Repository Manage Panel
|
repos.repo_manage_panel=Панель управления репозиторием
|
||||||
repos.owner=Владелец
|
repos.owner=Владелец
|
||||||
repos.name=Имя
|
repos.name=Имя
|
||||||
repos.private=Приватный
|
repos.private=Приватный
|
||||||
|
@ -826,47 +843,47 @@ repos.watches=Следят
|
||||||
repos.stars=В избранном
|
repos.stars=В избранном
|
||||||
repos.issues=Вопросы
|
repos.issues=Вопросы
|
||||||
|
|
||||||
auths.auth_manage_panel=Authentication Manage Panel
|
auths.auth_manage_panel=Панель управления аутнентификациями
|
||||||
auths.new=Add New Source
|
auths.new=Добавить новый источник
|
||||||
auths.name=Имя
|
auths.name=Имя
|
||||||
auths.type=Тип
|
auths.type=Тип
|
||||||
auths.enabled=Включено
|
auths.enabled=Включено
|
||||||
auths.updated=Обновлено
|
auths.updated=Обновлено
|
||||||
auths.auth_type=Authentication Type
|
auths.auth_type=Тип аутентификации
|
||||||
auths.auth_name=Authentication Name
|
auths.auth_name=Имя аутентификации
|
||||||
auths.domain=Домен
|
auths.domain=Домен
|
||||||
auths.host=Хост
|
auths.host=Хост
|
||||||
auths.port=Порт
|
auths.port=Порт
|
||||||
auths.bind_dn=Bind DN
|
auths.bind_dn=Привязать DN
|
||||||
auths.bind_password=Bind Password
|
auths.bind_password=Привязать пароль
|
||||||
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account.
|
auths.bind_password_helper=Внимание: Этот пароль сохранен в небезопасном виде. Не используйте высоко-привилегированную учетную запись.
|
||||||
auths.user_base=User Search Base
|
auths.user_base=База для поиска пользователя
|
||||||
auths.user_dn=User DN
|
auths.user_dn=DN пользователя
|
||||||
auths.attribute_name=First name attribute
|
auths.attribute_name=Имя аттрибута
|
||||||
auths.attribute_surname=Surname attribute
|
auths.attribute_surname=Фамилия аттрибута
|
||||||
auths.attribute_mail=E-mail attribute
|
auths.attribute_mail=Электронная почта аттрибута
|
||||||
auths.filter=User Filter
|
auths.filter=Фильтр пользователя
|
||||||
auths.admin_filter=Admin Filter
|
auths.admin_filter=Фильтр администратора
|
||||||
auths.ms_ad_sa=Ms Ad SA
|
auths.ms_ad_sa=Ms Ad SA
|
||||||
auths.smtp_auth=SMTP Authentication Type
|
auths.smtp_auth=Тип аутентификации SMTP
|
||||||
auths.smtphost=Узел SMTP
|
auths.smtphost=Узел SMTP
|
||||||
auths.smtpport=SMTP-порт
|
auths.smtpport=SMTP-порт
|
||||||
auths.allowed_domains=Allowed Domains
|
auths.allowed_domains=Разрешенные домены
|
||||||
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
|
auths.allowed_domains_helper=Оставьте пустым чтобы не ограничивать домены. Несколько доменов должны быть разделены запятыми ','.
|
||||||
auths.enable_tls=Включение шифрования TLS
|
auths.enable_tls=Включение шифрования TLS
|
||||||
auths.skip_tls_verify=Skip TLS Verify
|
auths.skip_tls_verify=Пропустить проверку TLS
|
||||||
auths.pam_service_name=PAM Service Name
|
auths.pam_service_name=Имя службы PAM
|
||||||
auths.enable_auto_register=Включить автоматическую регистрацию
|
auths.enable_auto_register=Включить автоматическую регистрацию
|
||||||
auths.tips=Советы
|
auths.tips=Советы
|
||||||
auths.edit=Edit Authentication Setting
|
auths.edit=Изменить параметры канала аутентификации
|
||||||
auths.activated=Эта аутентификация активирована
|
auths.activated=Эта аутентификация активирована
|
||||||
auths.new_success=New authentication '%s' has been added successfully.
|
auths.new_success=Новый канал аутентификации '%s' успешно создан.
|
||||||
auths.update_success=Authentication setting has been updated successfully.
|
auths.update_success=Настройки канала аутентификации успешно сохранены.
|
||||||
auths.update=Update Authentication Setting
|
auths.update=Обновить параметры аутентификации
|
||||||
auths.delete=Delete This Authentication
|
auths.delete=Удалить этот канал аутентификации
|
||||||
auths.delete_auth_title=Authentication Deletion
|
auths.delete_auth_title=Удаление канала аутентификации
|
||||||
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue?
|
auths.delete_auth_desc=Этот канал аутентификации будет удален. Вы уверены что хотите продолжить?
|
||||||
auths.deletion_success=Authentication has been deleted successfully!
|
auths.deletion_success=Канал аутентификации успешно удален!
|
||||||
|
|
||||||
config.server_config=Конфигурация сервера
|
config.server_config=Конфигурация сервера
|
||||||
config.app_name=Имя приложения
|
config.app_name=Имя приложения
|
||||||
|
@ -877,11 +894,11 @@ config.offline_mode=Автономный режим
|
||||||
config.disable_router_log=Отключение журнала маршрутизатора
|
config.disable_router_log=Отключение журнала маршрутизатора
|
||||||
config.run_user=Запуск пользователем
|
config.run_user=Запуск пользователем
|
||||||
config.run_mode=Режим выполнения
|
config.run_mode=Режим выполнения
|
||||||
config.repo_root_path=Repository Root Path
|
config.repo_root_path=Путь до корня репозитория
|
||||||
config.static_file_root_path=Static File Root Path
|
config.static_file_root_path=Статичный путь до файла
|
||||||
config.log_file_root_path=Log File Root Path
|
config.log_file_root_path=Путь до папки с логами
|
||||||
config.script_type=Тип сценария
|
config.script_type=Тип сценария
|
||||||
config.reverse_auth_user=Reverse Authentication User
|
config.reverse_auth_user=Заголовок с именем пользователя для авторизации на reverse proxy
|
||||||
config.db_config=Конфигурация базы данных
|
config.db_config=Конфигурация базы данных
|
||||||
config.db_type=Тип
|
config.db_type=Тип
|
||||||
config.db_host=Хост
|
config.db_host=Хост
|
||||||
|
@ -890,20 +907,20 @@ config.db_user=Пользователь
|
||||||
config.db_ssl_mode=Режим SSL
|
config.db_ssl_mode=Режим SSL
|
||||||
config.db_ssl_mode_helper=(только для «postgres»)
|
config.db_ssl_mode_helper=(только для «postgres»)
|
||||||
config.db_path=Путь
|
config.db_path=Путь
|
||||||
config.db_path_helper=(for "sqlite3" and "tidb")
|
config.db_path_helper=(для "SQLite3" и "TiDB")
|
||||||
config.service_config=Service Configuration
|
config.service_config=Сервисная конфигурация
|
||||||
config.register_email_confirm=Require E-mail Confirmation
|
config.register_email_confirm=Требуется подтверждение по электронной почте
|
||||||
config.disable_register=Отключить регистрацию
|
config.disable_register=Отключить регистрацию
|
||||||
config.show_registration_button=Show Register Button
|
config.show_registration_button=Показать кнопку регистрации
|
||||||
config.require_sign_in_view=Для просмотра необходима авторизация
|
config.require_sign_in_view=Для просмотра необходима авторизация
|
||||||
config.enable_cache_avatar=Кешировать аватар
|
config.enable_cache_avatar=Кешировать аватар
|
||||||
config.mail_notify=Почтовые уведомления
|
config.mail_notify=Почтовые уведомления
|
||||||
config.disable_key_size_check=Disable Minimum Key Size Check
|
config.disable_key_size_check=Отключить проверку на минимальный размер ключа
|
||||||
config.enable_captcha=Enable Captcha
|
config.enable_captcha=Включить капчу
|
||||||
config.active_code_lives=Active Code Lives
|
config.active_code_lives=Время жизни кода для активации
|
||||||
config.reset_password_code_lives=Reset Password Code Lives
|
config.reset_password_code_lives=Время жизни кода сброса пароля
|
||||||
config.webhook_config=Настройка автоматического обновления репозиции
|
config.webhook_config=Настройка автоматического обновления репозиции
|
||||||
config.queue_length=Queue Length
|
config.queue_length=Длина очереди
|
||||||
config.deliver_timeout=Задержка доставки
|
config.deliver_timeout=Задержка доставки
|
||||||
config.skip_tls_verify=Пропустить TLS проверка
|
config.skip_tls_verify=Пропустить TLS проверка
|
||||||
config.mailer_config=Настройки почты
|
config.mailer_config=Настройки почты
|
||||||
|
@ -915,20 +932,20 @@ config.mailer_user=Пользователь
|
||||||
config.oauth_config=Конфигурация OAuth
|
config.oauth_config=Конфигурация OAuth
|
||||||
config.oauth_enabled=Включено
|
config.oauth_enabled=Включено
|
||||||
config.cache_config=Настройки кеша
|
config.cache_config=Настройки кеша
|
||||||
config.cache_adapter=Cache Adapter
|
config.cache_adapter=Адаптер кэша
|
||||||
config.cache_interval=Cache Interval
|
config.cache_interval=Интервал кэширования
|
||||||
config.cache_conn=Cache Connection
|
config.cache_conn=Подключение кэша
|
||||||
config.session_config=Session Configuration
|
config.session_config=Конфигурация сессии
|
||||||
config.session_provider=Session Provider
|
config.session_provider=Провайдер сессии
|
||||||
config.provider_config=Provider Config
|
config.provider_config=Конфигурация провайдера
|
||||||
config.cookie_name=Имя файла cookie
|
config.cookie_name=Имя файла cookie
|
||||||
config.enable_set_cookie=Enable Set Cookie
|
config.enable_set_cookie=Включить установку cookies
|
||||||
config.gc_interval_time=GC Interval Time
|
config.gc_interval_time=Интервал работы сборщика мусора
|
||||||
config.session_life_time=Время жизни сессии
|
config.session_life_time=Время жизни сессии
|
||||||
config.https_only=Только HTTPS
|
config.https_only=Только HTTPS
|
||||||
config.cookie_life_time=Время жизни файла cookie
|
config.cookie_life_time=Время жизни файла cookie
|
||||||
config.picture_config=Настройка изображения
|
config.picture_config=Настройка изображения
|
||||||
config.picture_service=Picture Service
|
config.picture_service=Сервис изображений
|
||||||
config.disable_gravatar=Отключить Gravatar
|
config.disable_gravatar=Отключить Gravatar
|
||||||
config.log_config=Конфигурация журнала
|
config.log_config=Конфигурация журнала
|
||||||
config.log_mode=Режим журналирования
|
config.log_mode=Режим журналирования
|
||||||
|
@ -938,7 +955,7 @@ monitor.name=Имя
|
||||||
monitor.schedule=Расписание
|
monitor.schedule=Расписание
|
||||||
monitor.next=В следующий раз
|
monitor.next=В следующий раз
|
||||||
monitor.previous=Предыдущий раз
|
monitor.previous=Предыдущий раз
|
||||||
monitor.execute_times=Execute Times
|
monitor.execute_times=Количество выполнений
|
||||||
monitor.process=Запущенные процессы
|
monitor.process=Запущенные процессы
|
||||||
monitor.desc=Описание
|
monitor.desc=Описание
|
||||||
monitor.start=Момент начала
|
monitor.start=Момент начала
|
||||||
|
@ -953,40 +970,40 @@ notices.delete_success=Системное уведомление успешно
|
||||||
|
|
||||||
[action]
|
[action]
|
||||||
create_repo=создан репозиторий <a href="%s"> %s</a>
|
create_repo=создан репозиторий <a href="%s"> %s</a>
|
||||||
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
rename_repo=репозиторий переименован из <code>%[1]s</code>на <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=pushed to <a href="%s/src/%s">%[2]s</a> at <a href="%[1]s">%[3]s</a>
|
commit_repo=запушил <a href="%[1]s/src/%[2]s">%[3]s</a> в <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`opened issue <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`открытый вопрос <a href="%s/issues/%s">%s#%[2]</a>`
|
||||||
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`созданный пулл-реквест <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`commented on issue <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue=`прокомментировал(а) вопрос <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
merge_pull_request=`слил пул реквест <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
transfer_repo=transfered repository <code>%s</code> to <a href="%s">%s</a>
|
transfer_repo=перенес репозиторий <code>%s</code> в <a href="%s">%s</a>
|
||||||
push_tag=pushed tag <a href="%s/src/%s">%[2]s</a> to <a href="%[1]s">%[3]s</a>
|
push_tag=запушил тэг <a href="%s/src/%s">%[2]s</a> в <a href="%[1]s">%[3]s</a>
|
||||||
compare_2_commits=Просмотреть сравнение двух коммитов
|
compare_2_commits=Просмотреть сравнение двух коммитов
|
||||||
|
|
||||||
[tool]
|
[tool]
|
||||||
ago=назад
|
ago=назад
|
||||||
from_now=from now
|
from_now=с этого момента
|
||||||
now=сейчас
|
now=сейчас
|
||||||
1s=1 second %s
|
1s=1 секунду %s
|
||||||
1m=1 минута %s
|
1m=1 минута %s
|
||||||
1h=1 час %s
|
1h=1 час %s
|
||||||
1d=1 день %s
|
1d=1 день %s
|
||||||
1w=1 неделя %s
|
1w=1 неделя %s
|
||||||
1mon=1 month %s
|
1mon=1 месяц %s
|
||||||
1y=1 год %s
|
1y=1 год %s
|
||||||
seconds=%d секунд %s
|
seconds=%d секунд %s
|
||||||
minutes=%d минут %s
|
minutes=%d минут %s
|
||||||
hours=%d часов %s
|
hours=%d часов %s
|
||||||
days=%d дней %s
|
days=%d дней %s
|
||||||
weeks=%d weeks %s
|
weeks=недель %s: %d
|
||||||
months=%d months %s
|
months=месяцев %s: %d
|
||||||
years=%d years %s
|
years=лет %s: %d
|
||||||
raw_seconds=секунд
|
raw_seconds=секунд
|
||||||
raw_minutes=минут
|
raw_minutes=минут
|
||||||
|
|
||||||
[dropzone]
|
[dropzone]
|
||||||
default_message=Drop files here or click to upload.
|
default_message=Перетащите файл сюда, или кликните для загрузки.
|
||||||
invalid_input_type=You can't upload files of this type.
|
invalid_input_type=Вы не можете загружать файлы этого типа.
|
||||||
file_too_big=File size({{filesize}} MB) exceeds maximum size({{maxFilesize}} MB).
|
file_too_big=Размер файла ({{filesize}} МБ) больше чем максимальный размер ({{maxFilesize}} МБ).
|
||||||
remove_file=Remove file
|
remove_file=Удалить файл
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,6 @@ forgot_password=忘记密码
|
||||||
forget_password=忘记密码?
|
forget_password=忘记密码?
|
||||||
sign_up_now=还没帐户?马上注册。
|
sign_up_now=还没帐户?马上注册。
|
||||||
confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %d 小时内完成确认注册操作。
|
confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %d 小时内完成确认注册操作。
|
||||||
sign_in_to_account=登录到您的帐户
|
|
||||||
active_your_account=激活您的帐户
|
active_your_account=激活您的帐户
|
||||||
resent_limit_prompt=对不起,您请求发送激活邮件过于频繁,请等待 3 分钟后再试!
|
resent_limit_prompt=对不起,您请求发送激活邮件过于频繁,请等待 3 分钟后再试!
|
||||||
has_unconfirmed_mail=%s 您好,系统检测到您有一封发送至 <b>%s</b> 但未被确认的邮件。如果您未收到激活邮件,或需要重新发送,请单击下方的按钮。
|
has_unconfirmed_mail=%s 您好,系统检测到您有一封发送至 <b>%s</b> 但未被确认的邮件。如果您未收到激活邮件,或需要重新发送,请单击下方的按钮。
|
||||||
|
@ -165,6 +164,7 @@ activate_account=请激活您的帐户
|
||||||
activate_email=请验证您的邮箱地址
|
activate_email=请验证您的邮箱地址
|
||||||
reset_password=重置您的密码
|
reset_password=重置您的密码
|
||||||
register_success=注册成功,欢迎使用
|
register_success=注册成功,欢迎使用
|
||||||
|
register_notify=欢迎使用
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=确认操作
|
yes=确认操作
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=长度最小为 %s 个字符。
|
||||||
max_size_error=长度最大为 %s 个字符。
|
max_size_error=长度最大为 %s 个字符。
|
||||||
email_error=不是一个有效的邮箱地址。
|
email_error=不是一个有效的邮箱地址。
|
||||||
url_error=不是一个有效的 URL。
|
url_error=不是一个有效的 URL。
|
||||||
|
include_error=必须包含子字符串 '%s'。
|
||||||
unknown_error=未知错误:
|
unknown_error=未知错误:
|
||||||
captcha_incorrect=验证码未匹配。
|
captcha_incorrect=验证码未匹配。
|
||||||
password_not_match=密码与确认密码未匹配。
|
password_not_match=密码与确认密码未匹配。
|
||||||
|
@ -336,6 +337,7 @@ visibility=可见性
|
||||||
visiblity_helper=该仓库为 <span class="ui red text">私有的</span>
|
visiblity_helper=该仓库为 <span class="ui red text">私有的</span>
|
||||||
visiblity_helper_forced=网站管理员已强制要求所有新建仓库必须为 <span class="ui red text">私有的</span>
|
visiblity_helper_forced=网站管理员已强制要求所有新建仓库必须为 <span class="ui red text">私有的</span>
|
||||||
visiblity_fork_helper=(修改该值将会影响到所有派生仓库)
|
visiblity_fork_helper=(修改该值将会影响到所有派生仓库)
|
||||||
|
clone_helper=不知道如何操作?访问 <a target="_blank" href="%s">此处</a> 查看帮助!
|
||||||
fork_repo=派生仓库
|
fork_repo=派生仓库
|
||||||
fork_from=派生自
|
fork_from=派生自
|
||||||
fork_visiblity_helper=派生仓库无法修改可见性
|
fork_visiblity_helper=派生仓库无法修改可见性
|
||||||
|
@ -350,6 +352,9 @@ auto_init=使用选定的文件和模板初始化仓库
|
||||||
create_repo=创建仓库
|
create_repo=创建仓库
|
||||||
default_branch=默认分支
|
default_branch=默认分支
|
||||||
mirror_interval=镜像同步周期(小时)
|
mirror_interval=镜像同步周期(小时)
|
||||||
|
watchers=关注者
|
||||||
|
stargazers=称赞者
|
||||||
|
forks=派生仓库
|
||||||
|
|
||||||
form.name_reserved=仓库名称 '%s' 是被保留的。
|
form.name_reserved=仓库名称 '%s' 是被保留的。
|
||||||
form.name_pattern_not_allowed=仓库名称不允许 '%s' 的格式。
|
form.name_pattern_not_allowed=仓库名称不允许 '%s' 的格式。
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=该仓库将是一个 <span class="text blue">镜像</span>
|
||||||
migrate_repo=迁移仓库
|
migrate_repo=迁移仓库
|
||||||
migrate.clone_address=克隆地址
|
migrate.clone_address=克隆地址
|
||||||
migrate.clone_address_desc=该地址可以是 HTTP/HTTPS/GIT URL 或本地服务器路径。
|
migrate.clone_address_desc=该地址可以是 HTTP/HTTPS/GIT URL 或本地服务器路径。
|
||||||
|
migrate.permission_denied=您没有获得导入本地仓库的权限。
|
||||||
migrate.invalid_local_path=无效的本地路径,不存在或不是一个目录!
|
migrate.invalid_local_path=无效的本地路径,不存在或不是一个目录!
|
||||||
|
migrate.failed=迁移失败:%v
|
||||||
|
|
||||||
forked_from=派生自
|
forked_from=派生自
|
||||||
fork_from_self=无法派生已经拥有的仓库!
|
fork_from_self=无法派生已经拥有的仓库!
|
||||||
copy_link=复制链接
|
copy_link=复制链接
|
||||||
copy_link_success=复制成功!
|
copy_link_success=复制成功!
|
||||||
copy_link_error=请按下 ⌘-C 或 Ctrl-C 复制
|
copy_link_error=请按下 ⌘-C 或 Ctrl-C 复制
|
||||||
click_to_copy=复制到剪切板
|
|
||||||
copied=复制成功
|
copied=复制成功
|
||||||
clone_helper=不知道如何操作?访问 <a target="_blank" href="%s">此处</a> 查看帮助!
|
|
||||||
unwatch=取消关注
|
unwatch=取消关注
|
||||||
watch=关注
|
watch=关注
|
||||||
unstar=取消点赞
|
unstar=取消点赞
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=从命令行创建一个新的仓库
|
||||||
push_exist_repo=从命令行推送已经创建的仓库
|
push_exist_repo=从命令行推送已经创建的仓库
|
||||||
repo_is_empty=该仓库不包含任何内容,请稍后再进行访问!
|
repo_is_empty=该仓库不包含任何内容,请稍后再进行访问!
|
||||||
|
|
||||||
|
|
||||||
branch=分支
|
branch=分支
|
||||||
tree=目录树
|
tree=目录树
|
||||||
branch_and_tags=分支与标签
|
filter_branch_and_tag=过滤分支或标签
|
||||||
branches=分支列表
|
branches=分支列表
|
||||||
tags=标签列表
|
tags=标签列表
|
||||||
issues=工单管理
|
issues=工单管理
|
||||||
|
@ -455,9 +459,9 @@ issues.num_comments=%d 条评论
|
||||||
issues.commented_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 评论`
|
issues.commented_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 评论`
|
||||||
issues.no_content=这个人很懒,什么都没留下。
|
issues.no_content=这个人很懒,什么都没留下。
|
||||||
issues.close_issue=关闭
|
issues.close_issue=关闭
|
||||||
issues.close_comment_issue=关闭并评论
|
issues.close_comment_issue=评论并关闭
|
||||||
issues.reopen_issue=重新开启
|
issues.reopen_issue=重新开启
|
||||||
issues.reopen_comment_issue=重新开启并评论
|
issues.reopen_comment_issue=评论并重新开启
|
||||||
issues.create_comment=评论
|
issues.create_comment=评论
|
||||||
issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭`
|
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> 重新开启`
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=删除标签操作
|
||||||
issues.label_deletion_desc=删除该标签将会移除所有工单中相关的信息。是否继续?
|
issues.label_deletion_desc=删除该标签将会移除所有工单中相关的信息。是否继续?
|
||||||
issues.label_deletion_success=标签删除成功!
|
issues.label_deletion_success=标签删除成功!
|
||||||
|
|
||||||
|
pulls.new=创建合并请求
|
||||||
pulls.compare_changes=对比文件变化
|
pulls.compare_changes=对比文件变化
|
||||||
pulls.compare_changes_desc=对比两个分支间的文件变化并发起一个合并请求。
|
pulls.compare_changes_desc=对比两个分支间的文件变化并发起一个合并请求。
|
||||||
pulls.compare_base=基准分支
|
pulls.compare_base=基准分支
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=标题
|
||||||
milestones.desc=描述
|
milestones.desc=描述
|
||||||
milestones.due_date=截止日期(可选)
|
milestones.due_date=截止日期(可选)
|
||||||
milestones.clear=清除
|
milestones.clear=清除
|
||||||
milestones.invalid_due_date_format=截止日期的格式错误,必须是 'year-mm-dd' 的形式。
|
milestones.invalid_due_date_format=截止日期的格式错误,必须是 'yyyy-mm-dd' 的形式。
|
||||||
milestones.create_success=里程碑 '%s' 创建成功!
|
milestones.create_success=里程碑 '%s' 创建成功!
|
||||||
milestones.edit=编辑里程碑
|
milestones.edit=编辑里程碑
|
||||||
milestones.edit_subheader=使用更加清晰的描述来帮助人们更好地理解里程碑的作用。
|
milestones.edit_subheader=使用更加清晰的描述来帮助人们更好地理解里程碑的作用。
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=确认删除仓库
|
||||||
settings.add_collaborator=增加新的协作者
|
settings.add_collaborator=增加新的协作者
|
||||||
settings.add_collaborator_success=成功添加新的协作者!
|
settings.add_collaborator_success=成功添加新的协作者!
|
||||||
settings.remove_collaborator_success=被操作的协作者已经被收回权限!
|
settings.remove_collaborator_success=被操作的协作者已经被收回权限!
|
||||||
|
settings.search_user_placeholder=搜索用户...
|
||||||
settings.user_is_org_member=被操作的用户是组织成员,因此无法添加为协作者!
|
settings.user_is_org_member=被操作的用户是组织成员,因此无法添加为协作者!
|
||||||
settings.add_webhook=添加 Web 钩子
|
settings.add_webhook=添加 Web 钩子
|
||||||
settings.hooks_desc=Web 钩子允许您设定在 Gogs 上发生指定事件时对指定 URL 发送 POST 通知。查看 <a target="_blank" href="%s">Webhooks 文档</a> 获取更多信息。
|
settings.hooks_desc=Web 钩子允许您设定在 Gogs 上发生指定事件时对指定 URL 发送 POST 通知。查看 <a target="_blank" href="%s">Webhooks 文档</a> 获取更多信息。
|
||||||
|
@ -633,24 +639,32 @@ release.stable=稳定
|
||||||
release.edit=编辑
|
release.edit=编辑
|
||||||
release.ahead=在该版本发布之后已有 <strong>%d</strong> 次代码提交到 %s 分支
|
release.ahead=在该版本发布之后已有 <strong>%d</strong> 次代码提交到 %s 分支
|
||||||
release.source_code=源代码
|
release.source_code=源代码
|
||||||
|
release.new_subheader=发布版本对产品进行迭代。
|
||||||
|
release.edit_subheader=详细的变更日志可以帮助用户更好地了解产品做了哪些改进。
|
||||||
release.tag_name=标签名称
|
release.tag_name=标签名称
|
||||||
release.target=目标分支
|
release.target=目标分支
|
||||||
release.tag_helper=选择或创建一个已经存在的标签
|
release.tag_helper=选择或创建一个已经存在的标签
|
||||||
release.release_title=发布标题
|
release.title=标题
|
||||||
release.content_with_md=使用 <a href="%s">Markdown</a> 编辑内容
|
release.content=内容
|
||||||
release.write=内容编辑
|
release.write=内容编辑
|
||||||
release.preview=效果预览
|
release.preview=效果预览
|
||||||
release.content_placeholder=请输入内容
|
|
||||||
release.loading=正在加载...
|
release.loading=正在加载...
|
||||||
release.prerelease_desc=这是一个预发行版本
|
release.prerelease_desc=这是一个预发行版本
|
||||||
release.prerelease_helper=我们会告知用户不建议将本次发布投入生产环境使用。
|
release.prerelease_helper=我们会告知用户不建议将本次发布投入生产环境使用。
|
||||||
|
release.cancel=取消
|
||||||
release.publish=发布版本
|
release.publish=发布版本
|
||||||
release.save_draft=保存草稿
|
release.save_draft=保存草稿
|
||||||
release.edit_release=编辑发布信息
|
release.edit_release=编辑发布信息
|
||||||
|
release.delete_release=删除此次发布
|
||||||
|
release.deletion=删除版本发布操作
|
||||||
|
release.deletion_desc=删除该版本发布将会移除相应的 Git 标签。是否继续?
|
||||||
|
release.deletion_success=版本发布删除成功!
|
||||||
release.tag_name_already_exist=已经存在使用相同标签进行发布的版本。
|
release.tag_name_already_exist=已经存在使用相同标签进行发布的版本。
|
||||||
|
release.downloads=下载附件
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=组织名称
|
org_name_holder=组织名称
|
||||||
|
org_full_name_holder=组织全名
|
||||||
org_name_helper=伟大的组织都有一个简短而寓意深刻的名字。
|
org_name_helper=伟大的组织都有一个简短而寓意深刻的名字。
|
||||||
create_org=创建组织
|
create_org=创建组织
|
||||||
repo_updated=最后更新于
|
repo_updated=最后更新于
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=删除所有未激活帐户
|
||||||
dashboard.delete_inactivate_accounts_success=所有未激活帐号清除成功!
|
dashboard.delete_inactivate_accounts_success=所有未激活帐号清除成功!
|
||||||
dashboard.delete_repo_archives=删除所有仓库存档
|
dashboard.delete_repo_archives=删除所有仓库存档
|
||||||
dashboard.delete_repo_archives_success=所有仓库存档清除成功!
|
dashboard.delete_repo_archives_success=所有仓库存档清除成功!
|
||||||
|
dashboard.delete_missing_repos=删除所有丢失 Git 文件的仓库记录
|
||||||
|
dashboard.delete_missing_repos_success=所有丢失 Git 文件的仓库记录删除成功!
|
||||||
dashboard.git_gc_repos=对仓库进行垃圾回收
|
dashboard.git_gc_repos=对仓库进行垃圾回收
|
||||||
dashboard.git_gc_repos_success=所有仓库垃圾回收成功!
|
dashboard.git_gc_repos_success=所有仓库垃圾回收成功!
|
||||||
dashboard.resync_all_sshkeys=重新生成 '.ssh/authorized_keys' 文件(警告:不是 Gogs 的密钥也会被删除)
|
dashboard.resync_all_sshkeys=重新生成 '.ssh/authorized_keys' 文件(警告:不是 Gogs 的密钥也会被删除)
|
||||||
|
@ -806,7 +822,8 @@ users.update_profile_success=该用户信息更新成功!
|
||||||
users.edit_account=编辑用户信息
|
users.edit_account=编辑用户信息
|
||||||
users.is_activated=该用户已被激活
|
users.is_activated=该用户已被激活
|
||||||
users.is_admin=该用户具有管理员权限
|
users.is_admin=该用户具有管理员权限
|
||||||
users.allow_git_hook=该帐户具有创建 Git 钩子的权限
|
users.allow_git_hook=该用户具有创建 Git 钩子的权限
|
||||||
|
users.allow_import_local=该用户具有导入本地仓库的权限
|
||||||
users.update_profile=更新用户信息
|
users.update_profile=更新用户信息
|
||||||
users.delete_account=删除该用户
|
users.delete_account=删除该用户
|
||||||
users.still_own_repo=该帐户仍然是某些仓库的拥有者,您必须先转移或删除它们才能执行删除帐户操作!
|
users.still_own_repo=该帐户仍然是某些仓库的拥有者,您必须先转移或删除它们才能执行删除帐户操作!
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=系统提示删除成功!
|
||||||
[action]
|
[action]
|
||||||
create_repo=创建了仓库 <a href="%s">%s</a>
|
create_repo=创建了仓库 <a href="%s">%s</a>
|
||||||
rename_repo=重命名仓库 <code>%[1]s</code> 为 <a href="%[2]s">%[3]s</a>
|
rename_repo=重命名仓库 <code>%[1]s</code> 为 <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=推送了 <a href="%s/src/%s">%[2]s</a> 分支的代码到 <a href="%[1]s">%[3]s</a>
|
commit_repo=推送了 <a href="%[1]s/src/%[2]s">%[3]s</a> 分支的代码到 <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`创建了工单 <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`创建了工单 <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
create_pull_request=`创建了合并请求 <a href="%s/pulls/%s">%s#%[2]s</a>`
|
create_pull_request=`创建了合并请求 <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`评论了工单 <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue=`评论了工单 <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
|
|
|
@ -13,7 +13,7 @@ version=當前版本
|
||||||
page=頁面
|
page=頁面
|
||||||
template=模版
|
template=模版
|
||||||
language=語言選項
|
language=語言選項
|
||||||
create_new=Create...
|
create_new=創建...
|
||||||
user_profile_and_more=用戶信息及更多
|
user_profile_and_more=用戶信息及更多
|
||||||
signed_in_as=已登錄用戶
|
signed_in_as=已登錄用戶
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ admin_title=管理員帳號設置
|
||||||
admin_name=管理員用戶名
|
admin_name=管理員用戶名
|
||||||
admin_password=管理員密碼
|
admin_password=管理員密碼
|
||||||
confirm_password=確認密碼
|
confirm_password=確認密碼
|
||||||
admin_email=管理員郵箱
|
admin_email=Admin E-mail
|
||||||
install_gogs=立即安裝
|
install_gogs=立即安裝
|
||||||
test_git_failed=無法識別 'git' 命令:%v
|
test_git_failed=無法識別 'git' 命令:%v
|
||||||
sqlite3_not_available=您所使用的發行版本不支持 SQLite3,請從 %s 下載官方構建版,而不是 gobuild 版本。
|
sqlite3_not_available=您所使用的發行版本不支持 SQLite3,請從 %s 下載官方構建版,而不是 gobuild 版本。
|
||||||
|
@ -148,7 +148,6 @@ forgot_password=忘記密碼
|
||||||
forget_password=忘記密碼?
|
forget_password=忘記密碼?
|
||||||
sign_up_now=還沒帳戶?馬上註冊。
|
sign_up_now=還沒帳戶?馬上註冊。
|
||||||
confirmation_mail_sent_prompt=一封新的確認郵件已經被發送至 <b>%s</b>,請檢查您的收件箱並在 %d 小時內完成確認註冊操作。
|
confirmation_mail_sent_prompt=一封新的確認郵件已經被發送至 <b>%s</b>,請檢查您的收件箱並在 %d 小時內完成確認註冊操作。
|
||||||
sign_in_to_account=Sign in to your account
|
|
||||||
active_your_account=激活您的帳戶
|
active_your_account=激活您的帳戶
|
||||||
resent_limit_prompt=對不起,您請求發送激活郵件過於頻繁,請等待 3 分鐘後再試!
|
resent_limit_prompt=對不起,您請求發送激活郵件過於頻繁,請等待 3 分鐘後再試!
|
||||||
has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到激活郵件,或需要重新發送,請單擊下方的按鈕。
|
has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到激活郵件,或需要重新發送,請單擊下方的按鈕。
|
||||||
|
@ -165,6 +164,7 @@ activate_account=Please activate your account
|
||||||
activate_email=Verify your e-mail address
|
activate_email=Verify your e-mail address
|
||||||
reset_password=Reset your password
|
reset_password=Reset your password
|
||||||
register_success=Register success, Welcome
|
register_success=Register success, Welcome
|
||||||
|
register_notify=Welcome on board
|
||||||
|
|
||||||
[modal]
|
[modal]
|
||||||
yes=確認操作
|
yes=確認操作
|
||||||
|
@ -192,6 +192,7 @@ min_size_error=長度最小為 %s 個字符。
|
||||||
max_size_error=長度最大為 %s 個字符。
|
max_size_error=長度最大為 %s 個字符。
|
||||||
email_error=不是一個有效的郵箱地址。
|
email_error=不是一個有效的郵箱地址。
|
||||||
url_error=不是一個有效的 URL。
|
url_error=不是一個有效的 URL。
|
||||||
|
include_error=` must contain substring '%s'.`
|
||||||
unknown_error=未知錯誤:
|
unknown_error=未知錯誤:
|
||||||
captcha_incorrect=驗證碼未匹配。
|
captcha_incorrect=驗證碼未匹配。
|
||||||
password_not_match=密碼與確認密碼未匹配。
|
password_not_match=密碼與確認密碼未匹配。
|
||||||
|
@ -336,6 +337,7 @@ visibility=可見度
|
||||||
visiblity_helper=該倉庫為 <span class="ui red text">私有的</span>
|
visiblity_helper=該倉庫為 <span class="ui red text">私有的</span>
|
||||||
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
|
||||||
visiblity_fork_helper=(修改該值將會影響到所有派生倉庫)
|
visiblity_fork_helper=(修改該值將會影響到所有派生倉庫)
|
||||||
|
clone_helper=不知道如何操作?訪問 <a target="_blank"href="%s"> 帮助説明</a> !
|
||||||
fork_repo=派生倉庫
|
fork_repo=派生倉庫
|
||||||
fork_from=派生自
|
fork_from=派生自
|
||||||
fork_visiblity_helper=派生倉庫無法修改可見性。
|
fork_visiblity_helper=派生倉庫無法修改可見性。
|
||||||
|
@ -350,6 +352,9 @@ auto_init=Initialize this repository with selected files and template
|
||||||
create_repo=創建倉庫
|
create_repo=創建倉庫
|
||||||
default_branch=默認分支
|
default_branch=默認分支
|
||||||
mirror_interval=鏡像同步周期(小時)
|
mirror_interval=鏡像同步周期(小時)
|
||||||
|
watchers=Watchers
|
||||||
|
stargazers=Stargazers
|
||||||
|
forks=Forks
|
||||||
|
|
||||||
form.name_reserved=倉庫名稱 '%s' 是被保留的。
|
form.name_reserved=倉庫名稱 '%s' 是被保留的。
|
||||||
form.name_pattern_not_allowed=倉庫名稱不允許 '%s' 的格式。
|
form.name_pattern_not_allowed=倉庫名稱不允許 '%s' 的格式。
|
||||||
|
@ -360,16 +365,16 @@ migrate_type_helper=該倉庫將是一個 <span class="text blue">鏡像</span>
|
||||||
migrate_repo=遷移倉庫
|
migrate_repo=遷移倉庫
|
||||||
migrate.clone_address=複製地址
|
migrate.clone_address=複製地址
|
||||||
migrate.clone_address_desc=該地址可以是 HTTP/HTTPS/GIT URL 或本地服務器路徑。
|
migrate.clone_address_desc=該地址可以是 HTTP/HTTPS/GIT URL 或本地服務器路徑。
|
||||||
|
migrate.permission_denied=You are not allowed to import local repositories.
|
||||||
migrate.invalid_local_path=無效的本地路徑,該路徑不存在或不是一個目錄!
|
migrate.invalid_local_path=無效的本地路徑,該路徑不存在或不是一個目錄!
|
||||||
|
migrate.failed=Migration failed: %v
|
||||||
|
|
||||||
forked_from=派生自
|
forked_from=派生自
|
||||||
fork_from_self=無法派生已經擁有的倉庫!
|
fork_from_self=無法派生已經擁有的倉庫!
|
||||||
copy_link=複製連結
|
copy_link=複製連結
|
||||||
copy_link_success=Copied!
|
copy_link_success=Copied!
|
||||||
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
copy_link_error=Press ⌘-C or Ctrl-C to copy
|
||||||
click_to_copy=複製到剪切簿
|
|
||||||
copied=複製成功
|
copied=複製成功
|
||||||
clone_helper=不知道如何操作?訪問 <a target="_blank"href="%s"> 帮助説明</a> !
|
|
||||||
unwatch=取消關註
|
unwatch=取消關註
|
||||||
watch=關註
|
watch=關註
|
||||||
unstar=取消讚好
|
unstar=取消讚好
|
||||||
|
@ -383,10 +388,9 @@ create_new_repo_command=從命令行創建一個新的倉庫
|
||||||
push_exist_repo=從命令行推送已經創建的倉庫
|
push_exist_repo=從命令行推送已經創建的倉庫
|
||||||
repo_is_empty=This repository is empty, please come back later!
|
repo_is_empty=This repository is empty, please come back later!
|
||||||
|
|
||||||
|
|
||||||
branch=分支
|
branch=分支
|
||||||
tree=目錄樹
|
tree=目錄樹
|
||||||
branch_and_tags=分支與標籤
|
filter_branch_and_tag=Filter branch or tag
|
||||||
branches=分支列表
|
branches=分支列表
|
||||||
tags=標籤列表
|
tags=標籤列表
|
||||||
issues=問題管理
|
issues=問題管理
|
||||||
|
@ -481,6 +485,7 @@ issues.label_deletion=刪除標籤
|
||||||
issues.label_deletion_desc=刪除該標籤將會移除所有問題中相關的訊息。是否繼續?
|
issues.label_deletion_desc=刪除該標籤將會移除所有問題中相關的訊息。是否繼續?
|
||||||
issues.label_deletion_success=標籤刪除成功!
|
issues.label_deletion_success=標籤刪除成功!
|
||||||
|
|
||||||
|
pulls.new=New Pull Request
|
||||||
pulls.compare_changes=對比文件變化
|
pulls.compare_changes=對比文件變化
|
||||||
pulls.compare_changes_desc=對比兩個分支間的文件變化及發起一個合併請求。
|
pulls.compare_changes_desc=對比兩個分支間的文件變化及發起一個合併請求。
|
||||||
pulls.compare_base=base
|
pulls.compare_base=base
|
||||||
|
@ -519,7 +524,7 @@ milestones.title=標題
|
||||||
milestones.desc=描述
|
milestones.desc=描述
|
||||||
milestones.due_date=截止日期(可選)
|
milestones.due_date=截止日期(可選)
|
||||||
milestones.clear=清除
|
milestones.clear=清除
|
||||||
milestones.invalid_due_date_format=截止日期的格式錯誤,必須是 'year-mm-dd' 的形式。
|
milestones.invalid_due_date_format=截止日期的格式錯誤,必須是 'yyyy-mm-dd' 的形式。
|
||||||
milestones.create_success=里程碑 '%s' 創建成功!
|
milestones.create_success=里程碑 '%s' 創建成功!
|
||||||
milestones.edit=編輯里程碑
|
milestones.edit=編輯里程碑
|
||||||
milestones.edit_subheader=使用更加清晰的描述來幫助人們更好地理解里程碑的作用。
|
milestones.edit_subheader=使用更加清晰的描述來幫助人們更好地理解里程碑的作用。
|
||||||
|
@ -561,6 +566,7 @@ settings.confirm_delete=確認刪除倉庫
|
||||||
settings.add_collaborator=增加新的協作者
|
settings.add_collaborator=增加新的協作者
|
||||||
settings.add_collaborator_success=成功添加新的協作者!
|
settings.add_collaborator_success=成功添加新的協作者!
|
||||||
settings.remove_collaborator_success=被操作的協作者已經被收回權限!
|
settings.remove_collaborator_success=被操作的協作者已經被收回權限!
|
||||||
|
settings.search_user_placeholder=Search user...
|
||||||
settings.user_is_org_member=被操作的用戶是組織成員,因此無法添加為協作者!
|
settings.user_is_org_member=被操作的用戶是組織成員,因此無法添加為協作者!
|
||||||
settings.add_webhook=添加 Web 鉤子
|
settings.add_webhook=添加 Web 鉤子
|
||||||
settings.hooks_desc=Web 鉤子允許您設定在 Gogs 上發生指定事件時對指定 URL 發送 POST 通知。查看 <a target="_blank" href="%s">Webhooks 文檔</a> 獲取更多信息。
|
settings.hooks_desc=Web 鉤子允許您設定在 Gogs 上發生指定事件時對指定 URL 發送 POST 通知。查看 <a target="_blank" href="%s">Webhooks 文檔</a> 獲取更多信息。
|
||||||
|
@ -633,24 +639,32 @@ release.stable=穩定
|
||||||
release.edit=編輯
|
release.edit=編輯
|
||||||
release.ahead=在該版本發佈之後已有 <strong>%d</strong> 次代碼提交到 %s 分支
|
release.ahead=在該版本發佈之後已有 <strong>%d</strong> 次代碼提交到 %s 分支
|
||||||
release.source_code=源代碼
|
release.source_code=源代碼
|
||||||
|
release.new_subheader=Publish releases to iterate product.
|
||||||
|
release.edit_subheader=Detailed change log can help users understand what has been improved.
|
||||||
release.tag_name=標籤名稱
|
release.tag_name=標籤名稱
|
||||||
release.target=目標分支
|
release.target=目標分支
|
||||||
release.tag_helper=選擇或創建一個已存在的標籤
|
release.tag_helper=選擇或創建一個已存在的標籤
|
||||||
release.release_title=發佈標題
|
release.title=Title
|
||||||
release.content_with_md=使用 <a href="%s">Markdown</a> 編輯內容
|
release.content=Content
|
||||||
release.write=內容編輯
|
release.write=內容編輯
|
||||||
release.preview=效果預覽
|
release.preview=效果預覽
|
||||||
release.content_placeholder=請輸入內容
|
|
||||||
release.loading=正在加載...
|
release.loading=正在加載...
|
||||||
release.prerelease_desc=這是一個預發佈版本
|
release.prerelease_desc=這是一個預發佈版本
|
||||||
release.prerelease_helper=我們會告知用戶不建議將本發佈投入生產環境使用。
|
release.prerelease_helper=我們會告知用戶不建議將本發佈投入生產環境使用。
|
||||||
|
release.cancel=Cancel
|
||||||
release.publish=發佈版本
|
release.publish=發佈版本
|
||||||
release.save_draft=保在草稿
|
release.save_draft=保在草稿
|
||||||
release.edit_release=編輯發佈信息
|
release.edit_release=編輯發佈信息
|
||||||
|
release.delete_release=Delete This Release
|
||||||
|
release.deletion=Release Deletion
|
||||||
|
release.deletion_desc=Delete this release will delete corresponding Git tag. Do you want to continue?
|
||||||
|
release.deletion_success=Release has been deleted successfully!
|
||||||
release.tag_name_already_exist=已經存在使用相同標籤的發佈版本。
|
release.tag_name_already_exist=已經存在使用相同標籤的發佈版本。
|
||||||
|
release.downloads=Downloads
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=組織名稱
|
org_name_holder=組織名稱
|
||||||
|
org_full_name_holder=Organization Full Name
|
||||||
org_name_helper=偉大的組織都有一個簡短而寓意深刻的名字。
|
org_name_helper=偉大的組織都有一個簡短而寓意深刻的名字。
|
||||||
create_org=創建組織
|
create_org=創建組織
|
||||||
repo_updated=最後更新於
|
repo_updated=最後更新於
|
||||||
|
@ -751,6 +765,8 @@ dashboard.delete_inactivate_accounts=刪除所有未激活帳戶
|
||||||
dashboard.delete_inactivate_accounts_success=所有未激活帳號清除成功!
|
dashboard.delete_inactivate_accounts_success=所有未激活帳號清除成功!
|
||||||
dashboard.delete_repo_archives=刪除所有倉庫存檔
|
dashboard.delete_repo_archives=刪除所有倉庫存檔
|
||||||
dashboard.delete_repo_archives_success=所有倉庫存檔清除成功!
|
dashboard.delete_repo_archives_success=所有倉庫存檔清除成功!
|
||||||
|
dashboard.delete_missing_repos=Delete all repository records that lost Git files
|
||||||
|
dashboard.delete_missing_repos_success=All repository records that lost Git files have been deleted successfully.
|
||||||
dashboard.git_gc_repos=對倉庫進行垃圾回收
|
dashboard.git_gc_repos=對倉庫進行垃圾回收
|
||||||
dashboard.git_gc_repos_success=所有倉庫的垃圾回收已成功完成!
|
dashboard.git_gc_repos_success=所有倉庫的垃圾回收已成功完成!
|
||||||
dashboard.resync_all_sshkeys=重新生成 '.ssh/authorized_keys' 文件(警告:不是 Gogs 的密鑰也會被刪除)
|
dashboard.resync_all_sshkeys=重新生成 '.ssh/authorized_keys' 文件(警告:不是 Gogs 的密鑰也會被刪除)
|
||||||
|
@ -807,6 +823,7 @@ users.edit_account=編輯用戶信息
|
||||||
users.is_activated=該用戶已被激活
|
users.is_activated=該用戶已被激活
|
||||||
users.is_admin=該用戶具有管理員權限
|
users.is_admin=該用戶具有管理員權限
|
||||||
users.allow_git_hook=該帳戶具有創建 Git 鉤子的權限
|
users.allow_git_hook=該帳戶具有創建 Git 鉤子的權限
|
||||||
|
users.allow_import_local=This account has permissions to import local repositories
|
||||||
users.update_profile=更新用戶信息
|
users.update_profile=更新用戶信息
|
||||||
users.delete_account=刪除該用戶
|
users.delete_account=刪除該用戶
|
||||||
users.still_own_repo=該帳戶仍然是某些倉庫的擁有者,您必須先轉移或刪除它們才能執行刪除帳戶操作!
|
users.still_own_repo=該帳戶仍然是某些倉庫的擁有者,您必須先轉移或刪除它們才能執行刪除帳戶操作!
|
||||||
|
@ -954,7 +971,7 @@ notices.delete_success=系統提示刪除成功!
|
||||||
[action]
|
[action]
|
||||||
create_repo=創建了倉庫 <a href="%s">%s</a>
|
create_repo=創建了倉庫 <a href="%s">%s</a>
|
||||||
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
|
||||||
commit_repo=推送了 <a href="%s/src/%s">%[2]s</a> 分支的代碼到 <a href="%[1]s">%[3]s</a>
|
commit_repo=推送了 <a href="%[1]s/src/%[2]s">%[3]s</a> 分支的代碼到 <a href="%[1]s">%[4]s</a>
|
||||||
create_issue=`創建了問題 <a href="%s/issues/%s">%s#%[2]s</a>`
|
create_issue=`創建了問題 <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=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
|
||||||
comment_issue=`評論了問題 <a href="%s/issues/%s">%s#%[2]s</a>`
|
comment_issue=`評論了問題 <a href="%s/issues/%s">%s#%[2]s</a>`
|
||||||
|
|
|
@ -464,7 +464,7 @@
|
||||||
"ignore": 0,
|
"ignore": 0,
|
||||||
"ignoreWasSetByUser": 0,
|
"ignoreWasSetByUser": 0,
|
||||||
"inputAbbreviatedPath": "\/public\/less\/gogs.less",
|
"inputAbbreviatedPath": "\/public\/less\/gogs.less",
|
||||||
"outputAbbreviatedPath": "\/public\/css\/gogs.min.css",
|
"outputAbbreviatedPath": "\/public\/css\/gogs.css",
|
||||||
"outputPathIsOutsideProject": 0,
|
"outputPathIsOutsideProject": 0,
|
||||||
"outputPathIsSetByUser": 1,
|
"outputPathIsSetByUser": 1,
|
||||||
"outputStyle": 1,
|
"outputStyle": 1,
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
links:
|
|
||||||
- mysql
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
|
|
||||||
mysql:
|
|
||||||
image: mysql
|
|
||||||
environment:
|
|
||||||
- MYSQL_ROOT_PASSWORD=gogs
|
|
||||||
- MYSQL_DATABASE=gogs
|
|
|
@ -1,4 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
set -x
|
||||||
|
set -e
|
||||||
|
|
||||||
# Set temp environment vars
|
# Set temp environment vars
|
||||||
export GOPATH=/tmp/go
|
export GOPATH=/tmp/go
|
||||||
|
|
|
@ -20,3 +20,4 @@ ln -sf /data/gogs/data ./data
|
||||||
ln -sf /data/git /home/git
|
ln -sf /data/git /home/git
|
||||||
|
|
||||||
chown -R git:git /data /app/gogs ~git/
|
chown -R git:git /data /app/gogs ~git/
|
||||||
|
chmod 0755 /data /data/gogs ~git/
|
||||||
|
|
|
@ -23,4 +23,5 @@ fi
|
||||||
|
|
||||||
# Set correct right to ssh keys
|
# Set correct right to ssh keys
|
||||||
chown -R root:root /data/ssh/*
|
chown -R root:root /data/ssh/*
|
||||||
chmod 600 /data/ssh/*
|
chmod 0700 /data/ssh
|
||||||
|
chmod 0600 /data/ssh/*
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if test -f ./setup; then
|
||||||
|
source ./setup
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec gosu root /sbin/syslogd -nS -O-
|
|
@ -1,17 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Cleanup SOCAT services and s6 event folder
|
create_socat_links() {
|
||||||
# On start and on shutdown in case container has been killed
|
|
||||||
rm -rf $(find /app/gogs/docker/s6/ -name 'event')
|
|
||||||
rm -rf /app/gogs/docker/s6/SOCAT_*
|
|
||||||
|
|
||||||
# Create VOLUME subfolder
|
|
||||||
for f in /data/gogs/data /data/gogs/conf /data/gogs/log /data/git /data/ssh; do
|
|
||||||
if ! test -d $f; then
|
|
||||||
mkdir -p $f
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Bind linked docker container to localhost socket using socat
|
# Bind linked docker container to localhost socket using socat
|
||||||
USED_PORT="3000:22"
|
USED_PORT="3000:22"
|
||||||
while read NAME ADDR PORT; do
|
while read NAME ADDR PORT; do
|
||||||
|
@ -31,10 +20,37 @@ while read NAME ADDR PORT; do
|
||||||
done << EOT
|
done << EOT
|
||||||
$(env | sed -En 's|(.*)_PORT_([0-9]+)_TCP=tcp://(.*):([0-9]+)|\1 \3 \4|p')
|
$(env | sed -En 's|(.*)_PORT_([0-9]+)_TCP=tcp://(.*):([0-9]+)|\1 \3 \4|p')
|
||||||
EOT
|
EOT
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
# Cleanup SOCAT services and s6 event folder
|
||||||
|
# On start and on shutdown in case container has been killed
|
||||||
|
rm -rf $(find /app/gogs/docker/s6/ -name 'event')
|
||||||
|
rm -rf /app/gogs/docker/s6/SOCAT_*
|
||||||
|
}
|
||||||
|
|
||||||
|
create_volume_subfolder() {
|
||||||
|
# Create VOLUME subfolder
|
||||||
|
for f in /data/gogs/data /data/gogs/conf /data/gogs/log /data/git /data/ssh; do
|
||||||
|
if ! test -d $f; then
|
||||||
|
mkdir -p $f
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
create_volume_subfolder
|
||||||
|
|
||||||
|
LINK=$(echo "$SOCAT_LINK" | tr '[:upper:]' '[:lower:]')
|
||||||
|
if [ "$LINK" = "false" -o "$LINK" = "0" ]; then
|
||||||
|
echo "init:socat | Will not try to create socat links as requested" 1>&2
|
||||||
|
else
|
||||||
|
create_socat_links
|
||||||
|
fi
|
||||||
|
|
||||||
# Exec CMD or S6 by default if nothing present
|
# Exec CMD or S6 by default if nothing present
|
||||||
if [ $# -gt 0 ];then
|
if [ $# -gt 0 ];then
|
||||||
exec "$@"
|
exec "$@"
|
||||||
else
|
else
|
||||||
exec /usr/bin/s6-svscan /app/gogs/docker/s6/
|
exec /bin/s6-svscan /app/gogs/docker/s6/
|
||||||
fi
|
fi
|
||||||
|
|
4
gogs.go
4
gogs.go
|
@ -4,7 +4,7 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Gogs(Go Git Service) is a painless self-hosted Git Service written in Go.
|
// Gogs (Go Git Service) is a painless self-hosted Git Service.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -17,7 +17,7 @@ import (
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
const APP_VER = "0.6.18.1029 Beta"
|
const APP_VER = "0.7.20.1121 Beta"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
|
|
@ -36,7 +36,10 @@ func accessLevel(e Engine, u *User, repo *Repository) (AccessMode, error) {
|
||||||
mode = ACCESS_MODE_READ
|
mode = ACCESS_MODE_READ
|
||||||
}
|
}
|
||||||
|
|
||||||
if u != nil {
|
if u == nil {
|
||||||
|
return mode, nil
|
||||||
|
}
|
||||||
|
|
||||||
if u.Id == repo.OwnerID {
|
if u.Id == repo.OwnerID {
|
||||||
return ACCESS_MODE_OWNER, nil
|
return ACCESS_MODE_OWNER, nil
|
||||||
}
|
}
|
||||||
|
@ -48,9 +51,6 @@ func accessLevel(e Engine, u *User, repo *Repository) (AccessMode, error) {
|
||||||
return a.Mode, nil
|
return a.Mode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return mode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the
|
// AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the
|
||||||
// user does not have access. User can be nil!
|
// user does not have access. User can be nil!
|
||||||
func AccessLevel(u *User, repo *Repository) (AccessMode, error) {
|
func AccessLevel(u *User, repo *Repository) (AccessMode, error) {
|
||||||
|
@ -67,9 +67,8 @@ func HasAccess(u *User, repo *Repository, testMode AccessMode) (bool, error) {
|
||||||
return hasAccess(x, u, repo, testMode)
|
return hasAccess(x, u, repo, testMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccessibleRepositories finds all repositories where a user has access to,
|
// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own.
|
||||||
// besides he/she owns.
|
func (u *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) {
|
||||||
func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) {
|
|
||||||
accesses := make([]*Access, 0, 10)
|
accesses := make([]*Access, 0, 10)
|
||||||
if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil {
|
if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -80,7 +79,7 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) {
|
||||||
repo, err := GetRepositoryByID(access.RepoID)
|
repo, err := GetRepositoryByID(access.RepoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrRepoNotExist(err) {
|
if IsErrRepoNotExist(err) {
|
||||||
log.Error(4, "%v", err)
|
log.Error(4, "GetRepositoryByID: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -92,11 +91,28 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) {
|
||||||
}
|
}
|
||||||
repos[repo] = access.Mode
|
repos[repo] = access.Mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: should we generate an ordered list here? Random looks weird.
|
|
||||||
return repos, nil
|
return repos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAccessibleRepositories finds all repositories where a user has access but does not own.
|
||||||
|
func (u *User) GetAccessibleRepositories() ([]*Repository, error) {
|
||||||
|
accesses := make([]*Access, 0, 10)
|
||||||
|
if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(accesses) == 0 {
|
||||||
|
return []*Repository{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
repoIDs := make([]int64, 0, len(accesses))
|
||||||
|
for _, access := range accesses {
|
||||||
|
repoIDs = append(repoIDs, access.RepoID)
|
||||||
|
}
|
||||||
|
repos := make([]*Repository, 0, len(repoIDs))
|
||||||
|
return repos, x.Where("owner_id != ?", u.Id).In("id", repoIDs).Desc("updated").Find(&repos)
|
||||||
|
}
|
||||||
|
|
||||||
func maxAccessMode(modes ...AccessMode) AccessMode {
|
func maxAccessMode(modes ...AccessMode) AccessMode {
|
||||||
max := ACCESS_MODE_NONE
|
max := ACCESS_MODE_NONE
|
||||||
for _, mode := range modes {
|
for _, mode := range modes {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
|
||||||
api "github.com/gogits/go-gogs-client"
|
api "github.com/gogits/go-gogs-client"
|
||||||
|
@ -136,6 +137,26 @@ func (a Action) GetIssueInfos() []string {
|
||||||
return strings.SplitN(a.Content, "|", 2)
|
return strings.SplitN(a.Content, "|", 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a Action) GetIssueTitle() string {
|
||||||
|
index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
|
||||||
|
issue, err := GetIssueByIndex(a.RepoID, index)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "GetIssueByIndex: %v", err)
|
||||||
|
return "500 when get issue"
|
||||||
|
}
|
||||||
|
return issue.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Action) GetIssueContent() string {
|
||||||
|
index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
|
||||||
|
issue, err := GetIssueByIndex(a.RepoID, index)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "GetIssueByIndex: %v", err)
|
||||||
|
return "500 when get issue"
|
||||||
|
}
|
||||||
|
return issue.Content
|
||||||
|
}
|
||||||
|
|
||||||
func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
|
func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
|
||||||
if err = notifyWatchers(e, &Action{
|
if err = notifyWatchers(e, &Action{
|
||||||
ActUserID: u.Id,
|
ActUserID: u.Id,
|
||||||
|
@ -147,7 +168,7 @@ func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
|
||||||
RepoName: repo.Name,
|
RepoName: repo.Name,
|
||||||
IsPrivate: repo.IsPrivate,
|
IsPrivate: repo.IsPrivate,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("notify watchers '%d/%s': %v", u.Id, repo.ID, err)
|
return fmt.Errorf("notify watchers '%d/%d': %v", u.Id, repo.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name)
|
log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name)
|
||||||
|
@ -187,8 +208,48 @@ func issueIndexTrimRight(c rune) bool {
|
||||||
return !unicode.IsDigit(c)
|
return !unicode.IsDigit(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PushCommit struct {
|
||||||
|
Sha1 string
|
||||||
|
Message string
|
||||||
|
AuthorEmail string
|
||||||
|
AuthorName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushCommits struct {
|
||||||
|
Len int
|
||||||
|
Commits []*PushCommit
|
||||||
|
CompareUrl string
|
||||||
|
|
||||||
|
avatars map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPushCommits() *PushCommits {
|
||||||
|
return &PushCommits{
|
||||||
|
avatars: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AvatarLink tries to match user in database with e-mail
|
||||||
|
// in order to show custom avatar, and falls back to general avatar link.
|
||||||
|
func (push *PushCommits) AvatarLink(email string) string {
|
||||||
|
_, ok := push.avatars[email]
|
||||||
|
if !ok {
|
||||||
|
u, err := GetUserByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
push.avatars[email] = base.AvatarLink(email)
|
||||||
|
if !IsErrUserNotExist(err) {
|
||||||
|
log.Error(4, "GetUserByEmail: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
push.avatars[email] = u.AvatarLink()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return push.avatars[email]
|
||||||
|
}
|
||||||
|
|
||||||
// updateIssuesCommit checks if issues are manipulated by commit message.
|
// updateIssuesCommit checks if issues are manipulated by commit message.
|
||||||
func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*base.PushCommit) error {
|
func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*PushCommit) error {
|
||||||
// Commits are appended in the reverse order.
|
// Commits are appended in the reverse order.
|
||||||
for i := len(commits) - 1; i >= 0; i-- {
|
for i := len(commits) - 1; i >= 0; i-- {
|
||||||
c := commits[i]
|
c := commits[i]
|
||||||
|
@ -322,7 +383,7 @@ func CommitRepoAction(
|
||||||
repoID int64,
|
repoID int64,
|
||||||
repoUserName, repoName string,
|
repoUserName, repoName string,
|
||||||
refFullName string,
|
refFullName string,
|
||||||
commit *base.PushCommits,
|
commit *PushCommits,
|
||||||
oldCommitID string, newCommitID string) error {
|
oldCommitID string, newCommitID string) error {
|
||||||
|
|
||||||
u, err := GetUserByID(userID)
|
u, err := GetUserByID(userID)
|
||||||
|
@ -337,12 +398,18 @@ func CommitRepoAction(
|
||||||
return fmt.Errorf("GetOwner: %v", err)
|
return fmt.Errorf("GetOwner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change repository bare status and update last updated time.
|
||||||
|
repo.IsBare = false
|
||||||
|
if err = UpdateRepository(repo, false); err != nil {
|
||||||
|
return fmt.Errorf("UpdateRepository: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
isNewBranch := false
|
isNewBranch := false
|
||||||
opType := COMMIT_REPO
|
opType := COMMIT_REPO
|
||||||
// Check it's tag push or branch.
|
// Check it's tag push or branch.
|
||||||
if strings.HasPrefix(refFullName, "refs/tags/") {
|
if strings.HasPrefix(refFullName, "refs/tags/") {
|
||||||
opType = PUSH_TAG
|
opType = PUSH_TAG
|
||||||
commit = &base.PushCommits{}
|
commit = &PushCommits{}
|
||||||
} else {
|
} else {
|
||||||
// if not the first commit, set the compareUrl
|
// if not the first commit, set the compareUrl
|
||||||
if !strings.HasPrefix(oldCommitID, "0000000") {
|
if !strings.HasPrefix(oldCommitID, "0000000") {
|
||||||
|
@ -351,12 +418,10 @@ func CommitRepoAction(
|
||||||
isNewBranch = true
|
isNewBranch = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change repository bare status and update last updated time.
|
// NOTE: limit to detect latest 100 commits.
|
||||||
repo.IsBare = false
|
if len(commit.Commits) > 100 {
|
||||||
if err = UpdateRepository(repo, false); err != nil {
|
commit.Commits = commit.Commits[len(commit.Commits)-100:]
|
||||||
return fmt.Errorf("UpdateRepository: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits); err != nil {
|
if err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits); err != nil {
|
||||||
log.Error(4, "updateIssuesCommit: %v", err)
|
log.Error(4, "updateIssuesCommit: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -488,7 +553,7 @@ func transferRepoAction(e Engine, actUser, oldOwner, newOwner *User, repo *Repos
|
||||||
IsPrivate: repo.IsPrivate,
|
IsPrivate: repo.IsPrivate,
|
||||||
Content: path.Join(oldOwner.LowerName, repo.LowerName),
|
Content: path.Join(oldOwner.LowerName, repo.LowerName),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("notify watchers '%d/%s': %v", actUser.Id, repo.ID, err)
|
return fmt.Errorf("notify watchers '%d/%d': %v", actUser.Id, repo.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove watch for organization.
|
// Remove watch for organization.
|
||||||
|
|
112
models/error.go
112
models/error.go
|
@ -18,7 +18,7 @@ func IsErrNameReserved(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrNameReserved) Error() string {
|
func (err ErrNameReserved) Error() string {
|
||||||
return fmt.Sprintf("name is reserved: [name: %s]", err.Name)
|
return fmt.Sprintf("name is reserved [name: %s]", err.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrNamePatternNotAllowed struct {
|
type ErrNamePatternNotAllowed struct {
|
||||||
|
@ -31,7 +31,7 @@ func IsErrNamePatternNotAllowed(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrNamePatternNotAllowed) Error() string {
|
func (err ErrNamePatternNotAllowed) Error() string {
|
||||||
return fmt.Sprintf("name pattern is not allowed: [pattern: %s]", err.Pattern)
|
return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ____ ___
|
// ____ ___
|
||||||
|
@ -51,7 +51,7 @@ func IsErrUserAlreadyExist(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrUserAlreadyExist) Error() string {
|
func (err ErrUserAlreadyExist) Error() string {
|
||||||
return fmt.Sprintf("user already exists: [name: %s]", err.Name)
|
return fmt.Sprintf("user already exists [name: %s]", err.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrUserNotExist struct {
|
type ErrUserNotExist struct {
|
||||||
|
@ -65,7 +65,7 @@ func IsErrUserNotExist(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrUserNotExist) Error() string {
|
func (err ErrUserNotExist) Error() string {
|
||||||
return fmt.Sprintf("user does not exist: [uid: %d, name: %s]", err.UID, err.Name)
|
return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrEmailAlreadyUsed struct {
|
type ErrEmailAlreadyUsed struct {
|
||||||
|
@ -78,7 +78,7 @@ func IsErrEmailAlreadyUsed(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrEmailAlreadyUsed) Error() string {
|
func (err ErrEmailAlreadyUsed) Error() string {
|
||||||
return fmt.Sprintf("e-mail has been used: [email: %s]", err.Email)
|
return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrUserOwnRepos struct {
|
type ErrUserOwnRepos struct {
|
||||||
|
@ -91,7 +91,7 @@ func IsErrUserOwnRepos(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrUserOwnRepos) Error() string {
|
func (err ErrUserOwnRepos) Error() string {
|
||||||
return fmt.Sprintf("user still has ownership of repositories: [uid: %d]", err.UID)
|
return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrUserHasOrgs struct {
|
type ErrUserHasOrgs struct {
|
||||||
|
@ -104,7 +104,7 @@ func IsErrUserHasOrgs(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrUserHasOrgs) Error() string {
|
func (err ErrUserHasOrgs) Error() string {
|
||||||
return fmt.Sprintf("user still has membership of organizations: [uid: %d]", err.UID)
|
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// __________ ___. .__ .__ ____ __.
|
// __________ ___. .__ .__ ____ __.
|
||||||
|
@ -114,6 +114,19 @@ func (err ErrUserHasOrgs) Error() string {
|
||||||
// |____| |____/|___ /____/__|\___ > |____|__ \___ > ____|
|
// |____| |____/|___ /____/__|\___ > |____|__ \___ > ____|
|
||||||
// \/ \/ \/ \/\/
|
// \/ \/ \/ \/\/
|
||||||
|
|
||||||
|
type ErrKeyUnableVerify struct {
|
||||||
|
Result string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrKeyUnableVerify(err error) bool {
|
||||||
|
_, ok := err.(ErrKeyUnableVerify)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrKeyUnableVerify) Error() string {
|
||||||
|
return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
|
||||||
|
}
|
||||||
|
|
||||||
type ErrKeyNotExist struct {
|
type ErrKeyNotExist struct {
|
||||||
ID int64
|
ID int64
|
||||||
}
|
}
|
||||||
|
@ -124,7 +137,7 @@ func IsErrKeyNotExist(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrKeyNotExist) Error() string {
|
func (err ErrKeyNotExist) Error() string {
|
||||||
return fmt.Sprintf("public key does not exist: [id: %d]", err.ID)
|
return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrKeyAlreadyExist struct {
|
type ErrKeyAlreadyExist struct {
|
||||||
|
@ -138,7 +151,7 @@ func IsErrKeyAlreadyExist(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrKeyAlreadyExist) Error() string {
|
func (err ErrKeyAlreadyExist) Error() string {
|
||||||
return fmt.Sprintf("public key already exists: [owner_id: %d, content: %s]", err.OwnerID, err.Content)
|
return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrKeyNameAlreadyUsed struct {
|
type ErrKeyNameAlreadyUsed struct {
|
||||||
|
@ -152,7 +165,22 @@ func IsErrKeyNameAlreadyUsed(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrKeyNameAlreadyUsed) Error() string {
|
func (err ErrKeyNameAlreadyUsed) Error() string {
|
||||||
return fmt.Sprintf("public key already exists: [owner_id: %d, name: %s]", err.OwnerID, err.Name)
|
return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrDeployKeyNotExist struct {
|
||||||
|
ID int64
|
||||||
|
KeyID int64
|
||||||
|
RepoID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrDeployKeyNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrDeployKeyNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrDeployKeyNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrDeployKeyAlreadyExist struct {
|
type ErrDeployKeyAlreadyExist struct {
|
||||||
|
@ -166,7 +194,7 @@ func IsErrDeployKeyAlreadyExist(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrDeployKeyAlreadyExist) Error() string {
|
func (err ErrDeployKeyAlreadyExist) Error() string {
|
||||||
return fmt.Sprintf("public key already exists: [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
|
return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrDeployKeyNameAlreadyUsed struct {
|
type ErrDeployKeyNameAlreadyUsed struct {
|
||||||
|
@ -180,7 +208,7 @@ func IsErrDeployKeyNameAlreadyUsed(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrDeployKeyNameAlreadyUsed) Error() string {
|
func (err ErrDeployKeyNameAlreadyUsed) Error() string {
|
||||||
return fmt.Sprintf("public key already exists: [repo_id: %d, name: %s]", err.RepoID, err.Name)
|
return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// _____ ___________ __
|
// _____ ___________ __
|
||||||
|
@ -200,7 +228,7 @@ func IsErrAccessTokenNotExist(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrAccessTokenNotExist) Error() string {
|
func (err ErrAccessTokenNotExist) Error() string {
|
||||||
return fmt.Sprintf("access token does not exist: [sha: %s]", err.SHA)
|
return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ________ .__ __ .__
|
// ________ .__ __ .__
|
||||||
|
@ -220,7 +248,7 @@ func IsErrLastOrgOwner(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrLastOrgOwner) Error() string {
|
func (err ErrLastOrgOwner) Error() string {
|
||||||
return fmt.Sprintf("user is the last member of owner team: [uid: %d]", err.UID)
|
return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// __________ .__ __
|
// __________ .__ __
|
||||||
|
@ -259,6 +287,62 @@ func (err ErrRepoAlreadyExist) Error() string {
|
||||||
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
|
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrInvalidCloneAddr struct {
|
||||||
|
IsURLError bool
|
||||||
|
IsInvalidPath bool
|
||||||
|
IsPermissionDenied bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrInvalidCloneAddr(err error) bool {
|
||||||
|
_, ok := err.(ErrInvalidCloneAddr)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrInvalidCloneAddr) Error() string {
|
||||||
|
return fmt.Sprintf("invalid clone address [is_url_error: %v, is_invalid_path: %v, is_permission_denied: %v]",
|
||||||
|
err.IsURLError, err.IsInvalidPath, err.IsPermissionDenied)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrUpdateTaskNotExist struct {
|
||||||
|
UUID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrUpdateTaskNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrUpdateTaskNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUpdateTaskNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("update task does not exist [uuid: %s]", err.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrReleaseAlreadyExist struct {
|
||||||
|
TagName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrReleaseAlreadyExist(err error) bool {
|
||||||
|
_, ok := err.(ErrReleaseAlreadyExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrReleaseAlreadyExist) Error() string {
|
||||||
|
return fmt.Sprintf("Release tag already exist [tag_name: %s]", err.TagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrReleaseNotExist struct {
|
||||||
|
ID int64
|
||||||
|
TagName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrReleaseNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrReleaseNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrReleaseNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("Release tag does not exist [id: %d, tag_name: %s]", err.ID, err.TagName)
|
||||||
|
}
|
||||||
|
|
||||||
// __ __ ___. .__ __
|
// __ __ ___. .__ __
|
||||||
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
|
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
|
||||||
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
|
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
|
||||||
|
|
|
@ -37,6 +37,7 @@ const (
|
||||||
DIFF_FILE_ADD = iota + 1
|
DIFF_FILE_ADD = iota + 1
|
||||||
DIFF_FILE_CHANGE
|
DIFF_FILE_CHANGE
|
||||||
DIFF_FILE_DEL
|
DIFF_FILE_DEL
|
||||||
|
DIFF_FILE_RENAME
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiffLine struct {
|
type DiffLine struct {
|
||||||
|
@ -57,12 +58,14 @@ type DiffSection struct {
|
||||||
|
|
||||||
type DiffFile struct {
|
type DiffFile struct {
|
||||||
Name string
|
Name string
|
||||||
|
OldName string
|
||||||
Index int
|
Index int
|
||||||
Addition, Deletion int
|
Addition, Deletion int
|
||||||
Type int
|
Type int
|
||||||
IsCreated bool
|
IsCreated bool
|
||||||
IsDeleted bool
|
IsDeleted bool
|
||||||
IsBin bool
|
IsBin bool
|
||||||
|
IsRenamed bool
|
||||||
Sections []*DiffSection
|
Sections []*DiffSection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +97,7 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff
|
||||||
var i int
|
var i int
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
// fmt.Println(i, line)
|
|
||||||
if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") {
|
if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -158,17 +161,27 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff
|
||||||
|
|
||||||
// Get new file.
|
// Get new file.
|
||||||
if strings.HasPrefix(line, DIFF_HEAD) {
|
if strings.HasPrefix(line, DIFF_HEAD) {
|
||||||
beg := len(DIFF_HEAD)
|
middle := -1
|
||||||
a := line[beg : (len(line)-beg)/2+beg]
|
|
||||||
|
|
||||||
// In case file name is surrounded by double quotes(it happens only in git-shell).
|
// Note: In case file name is surrounded by double quotes (it happens only in git-shell).
|
||||||
if a[0] == '"' {
|
// e.g. diff --git "a/xxx" "b/xxx"
|
||||||
a = a[1 : len(a)-1]
|
hasQuote := line[len(DIFF_HEAD)] == '"'
|
||||||
a = strings.Replace(a, `\"`, `"`, -1)
|
if hasQuote {
|
||||||
|
middle = strings.Index(line, ` "b/`)
|
||||||
|
} else {
|
||||||
|
middle = strings.Index(line, " b/")
|
||||||
|
}
|
||||||
|
|
||||||
|
beg := len(DIFF_HEAD)
|
||||||
|
a := line[beg+2 : middle]
|
||||||
|
b := line[middle+3:]
|
||||||
|
if hasQuote {
|
||||||
|
a = string(git.UnescapeChars([]byte(a[1 : len(a)-1])))
|
||||||
|
b = string(git.UnescapeChars([]byte(b[1 : len(b)-1])))
|
||||||
}
|
}
|
||||||
|
|
||||||
curFile = &DiffFile{
|
curFile = &DiffFile{
|
||||||
Name: a[strings.Index(a, "/")+1:],
|
Name: a,
|
||||||
Index: len(diff.Files) + 1,
|
Index: len(diff.Files) + 1,
|
||||||
Type: DIFF_FILE_CHANGE,
|
Type: DIFF_FILE_CHANGE,
|
||||||
Sections: make([]*DiffSection, 0, 10),
|
Sections: make([]*DiffSection, 0, 10),
|
||||||
|
@ -180,16 +193,17 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(scanner.Text(), "new file"):
|
case strings.HasPrefix(scanner.Text(), "new file"):
|
||||||
curFile.Type = DIFF_FILE_ADD
|
curFile.Type = DIFF_FILE_ADD
|
||||||
curFile.IsDeleted = false
|
|
||||||
curFile.IsCreated = true
|
curFile.IsCreated = true
|
||||||
case strings.HasPrefix(scanner.Text(), "deleted"):
|
case strings.HasPrefix(scanner.Text(), "deleted"):
|
||||||
curFile.Type = DIFF_FILE_DEL
|
curFile.Type = DIFF_FILE_DEL
|
||||||
curFile.IsCreated = false
|
|
||||||
curFile.IsDeleted = true
|
curFile.IsDeleted = true
|
||||||
case strings.HasPrefix(scanner.Text(), "index"):
|
case strings.HasPrefix(scanner.Text(), "index"):
|
||||||
curFile.Type = DIFF_FILE_CHANGE
|
curFile.Type = DIFF_FILE_CHANGE
|
||||||
curFile.IsCreated = false
|
case strings.HasPrefix(scanner.Text(), "similarity index 100%"):
|
||||||
curFile.IsDeleted = false
|
curFile.Type = DIFF_FILE_RENAME
|
||||||
|
curFile.IsRenamed = true
|
||||||
|
curFile.OldName = curFile.Name
|
||||||
|
curFile.Name = b
|
||||||
}
|
}
|
||||||
if curFile.Type > 0 {
|
if curFile.Type > 0 {
|
||||||
break
|
break
|
||||||
|
@ -244,10 +258,10 @@ func GetDiffRange(repoPath, beforeCommitId string, afterCommitId string, maxline
|
||||||
cmd = exec.Command("git", "show", afterCommitId)
|
cmd = exec.Command("git", "show", afterCommitId)
|
||||||
} else {
|
} else {
|
||||||
c, _ := commit.Parent(0)
|
c, _ := commit.Parent(0)
|
||||||
cmd = exec.Command("git", "diff", c.Id.String(), afterCommitId)
|
cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cmd = exec.Command("git", "diff", beforeCommitId, afterCommitId)
|
cmd = exec.Command("git", "diff", "-M", beforeCommitId, afterCommitId)
|
||||||
}
|
}
|
||||||
cmd.Dir = repoPath
|
cmd.Dir = repoPath
|
||||||
cmd.Stdout = wr
|
cmd.Stdout = wr
|
||||||
|
|
|
@ -718,32 +718,28 @@ func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
|
||||||
if opts.AssigneeID > 0 {
|
if opts.AssigneeID > 0 {
|
||||||
baseCond += " AND assignee_id=" + com.ToStr(opts.AssigneeID)
|
baseCond += " AND assignee_id=" + com.ToStr(opts.AssigneeID)
|
||||||
}
|
}
|
||||||
if opts.IsPull {
|
baseCond += " AND issue.is_pull=?"
|
||||||
baseCond += " AND issue.is_pull=1"
|
|
||||||
} else {
|
|
||||||
baseCond += " AND issue.is_pull=0"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch opts.FilterMode {
|
switch opts.FilterMode {
|
||||||
case FM_ALL, FM_ASSIGN:
|
case FM_ALL, FM_ASSIGN:
|
||||||
results, _ := x.Query(queryStr+baseCond, false)
|
results, _ := x.Query(queryStr+baseCond, false, opts.IsPull)
|
||||||
stats.OpenCount = parseCountResult(results)
|
stats.OpenCount = parseCountResult(results)
|
||||||
results, _ = x.Query(queryStr+baseCond, true)
|
results, _ = x.Query(queryStr+baseCond, true, opts.IsPull)
|
||||||
stats.ClosedCount = parseCountResult(results)
|
stats.ClosedCount = parseCountResult(results)
|
||||||
|
|
||||||
case FM_CREATE:
|
case FM_CREATE:
|
||||||
baseCond += " AND poster_id=?"
|
baseCond += " AND poster_id=?"
|
||||||
results, _ := x.Query(queryStr+baseCond, false, opts.UserID)
|
results, _ := x.Query(queryStr+baseCond, false, opts.IsPull, opts.UserID)
|
||||||
stats.OpenCount = parseCountResult(results)
|
stats.OpenCount = parseCountResult(results)
|
||||||
results, _ = x.Query(queryStr+baseCond, true, opts.UserID)
|
results, _ = x.Query(queryStr+baseCond, true, opts.IsPull, opts.UserID)
|
||||||
stats.ClosedCount = parseCountResult(results)
|
stats.ClosedCount = parseCountResult(results)
|
||||||
|
|
||||||
case FM_MENTION:
|
case FM_MENTION:
|
||||||
queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id"
|
queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id"
|
||||||
baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?"
|
baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?"
|
||||||
results, _ := x.Query(queryStr+baseCond, false, opts.UserID, true)
|
results, _ := x.Query(queryStr+baseCond, false, opts.IsPull, opts.UserID, true)
|
||||||
stats.OpenCount = parseCountResult(results)
|
stats.OpenCount = parseCountResult(results)
|
||||||
results, _ = x.Query(queryStr+baseCond, true, opts.UserID, true)
|
results, _ = x.Query(queryStr+baseCond, true, opts.IsPull, opts.UserID, true)
|
||||||
stats.ClosedCount = parseCountResult(results)
|
stats.ClosedCount = parseCountResult(results)
|
||||||
}
|
}
|
||||||
return stats
|
return stats
|
||||||
|
@ -1375,8 +1371,8 @@ func ChangeMilestoneAssign(oldMid int64, issue *Issue) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMilestoneByID deletes a milestone by given ID.
|
// DeleteMilestoneByID deletes a milestone by given ID.
|
||||||
func DeleteMilestoneByID(mid int64) error {
|
func DeleteMilestoneByID(id int64) error {
|
||||||
m, err := GetMilestoneByID(mid)
|
m, err := GetMilestoneByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrMilestoneNotExist(err) {
|
if IsErrMilestoneNotExist(err) {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -225,10 +225,9 @@ func DeleteSource(source *LoginSource) error {
|
||||||
// |_______ \/_______ /\____|__ /____|
|
// |_______ \/_______ /\____|__ /____|
|
||||||
// \/ \/ \/
|
// \/ \/ \/
|
||||||
|
|
||||||
// Query if name/passwd can login against the LDAP directory pool
|
// LoginUserLDAPSource queries if name/passwd can login against the LDAP directory pool,
|
||||||
// Create a local user if success
|
// and create a local user if success when enabled.
|
||||||
// Return the same LoginUserPlain semantic
|
// It returns the same LoginUserPlain semantic.
|
||||||
// FIXME: https://github.com/gogits/gogs/issues/672
|
|
||||||
func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, autoRegister bool) (*User, error) {
|
func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, autoRegister bool) (*User, error) {
|
||||||
cfg := source.Cfg.(*LDAPConfig)
|
cfg := source.Cfg.(*LDAPConfig)
|
||||||
directBind := (source.Type == DLDAP)
|
directBind := (source.Type == DLDAP)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -66,6 +67,7 @@ var migrations = []Migration{
|
||||||
NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4
|
NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4
|
||||||
NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4
|
NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4
|
||||||
NewMigration("rename pull request fields", renamePullRequestFields), // V8 -> V9:v0.6.16
|
NewMigration("rename pull request fields", renamePullRequestFields), // V8 -> V9:v0.6.16
|
||||||
|
NewMigration("clean up migrate repo info", cleanUpMigrateRepoInfo), // V9 -> V10:v0.6.20
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
@ -454,7 +456,7 @@ func trimCommitActionAppUrlPrefix(x *xorm.Engine) error {
|
||||||
|
|
||||||
pushCommits = new(PushCommits)
|
pushCommits = new(PushCommits)
|
||||||
if err = json.Unmarshal(action["content"], pushCommits); err != nil {
|
if err = json.Unmarshal(action["content"], pushCommits); err != nil {
|
||||||
return fmt.Errorf("unmarshal action content[%s]: %v", actID, err)
|
return fmt.Errorf("unmarshal action content[%d]: %v", actID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
infos := strings.Split(pushCommits.CompareUrl, "/")
|
infos := strings.Split(pushCommits.CompareUrl, "/")
|
||||||
|
@ -465,7 +467,7 @@ func trimCommitActionAppUrlPrefix(x *xorm.Engine) error {
|
||||||
|
|
||||||
p, err := json.Marshal(pushCommits)
|
p, err := json.Marshal(pushCommits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal action content[%s]: %v", actID, err)
|
return fmt.Errorf("marshal action content[%d]: %v", actID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = sess.Id(actID).Update(&Action{
|
if _, err = sess.Id(actID).Update(&Action{
|
||||||
|
@ -653,3 +655,50 @@ func renamePullRequestFields(x *xorm.Engine) (err error) {
|
||||||
|
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanUpMigrateRepoInfo(x *xorm.Engine) (err error) {
|
||||||
|
type (
|
||||||
|
User struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
LowerName string
|
||||||
|
}
|
||||||
|
Repository struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
OwnerID int64
|
||||||
|
LowerName string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
repos := make([]*Repository, 0, 25)
|
||||||
|
if err = x.Where("is_mirror=?", false).Find(&repos); err != nil {
|
||||||
|
return fmt.Errorf("select all non-mirror repositories: %v", err)
|
||||||
|
}
|
||||||
|
var user *User
|
||||||
|
for _, repo := range repos {
|
||||||
|
user = &User{ID: repo.OwnerID}
|
||||||
|
has, err := x.Get(user)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get owner of repository[%d - %d]: %v", repo.ID, repo.OwnerID, err)
|
||||||
|
} else if !has {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(setting.RepoRootPath, user.LowerName, repo.LowerName+".git/config")
|
||||||
|
|
||||||
|
// In case repository file is somehow missing.
|
||||||
|
if !com.IsFile(configPath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := ini.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open config file: %v", err)
|
||||||
|
}
|
||||||
|
cfg.DeleteSection("remote \"origin\"")
|
||||||
|
if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
|
||||||
|
return fmt.Errorf("save config file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ func init() {
|
||||||
new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
|
new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
|
||||||
new(Notice), new(EmailAddress))
|
new(Notice), new(EmailAddress))
|
||||||
|
|
||||||
gonicNames := []string{"UID", "SSL"}
|
gonicNames := []string{"SSL"}
|
||||||
for _, name := range gonicNames {
|
for _, name := range gonicNames {
|
||||||
core.LintGonicMapper[name] = true
|
core.LintGonicMapper[name] = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -33,25 +32,8 @@ const (
|
||||||
_TPL_PUBLICK_KEY = `command="%s serv key-%d --config='%s'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
|
_TPL_PUBLICK_KEY = `command="%s serv key-%d --config='%s'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrKeyUnableVerify = errors.New("Unable to verify public key")
|
|
||||||
)
|
|
||||||
|
|
||||||
var sshOpLocker = sync.Mutex{}
|
var sshOpLocker = sync.Mutex{}
|
||||||
|
var SSHPath string // SSH directory.
|
||||||
var (
|
|
||||||
SSHPath string // SSH directory.
|
|
||||||
appPath string // Execution(binary) path.
|
|
||||||
)
|
|
||||||
|
|
||||||
// exePath returns the executable path.
|
|
||||||
func exePath() (string, error) {
|
|
||||||
file, err := exec.LookPath(os.Args[0])
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Abs(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// homeDir returns the home directory of current user.
|
// homeDir returns the home directory of current user.
|
||||||
func homeDir() string {
|
func homeDir() string {
|
||||||
|
@ -63,16 +45,9 @@ func homeDir() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
|
||||||
|
|
||||||
if appPath, err = exePath(); err != nil {
|
|
||||||
log.Fatal(4, "fail to get app path: %v\n", err)
|
|
||||||
}
|
|
||||||
appPath = strings.Replace(appPath, "\\", "/", -1)
|
|
||||||
|
|
||||||
// Determine and create .ssh path.
|
// Determine and create .ssh path.
|
||||||
SSHPath = filepath.Join(homeDir(), ".ssh")
|
SSHPath = filepath.Join(homeDir(), ".ssh")
|
||||||
if err = os.MkdirAll(SSHPath, 0700); err != nil {
|
if err := os.MkdirAll(SSHPath, 0700); err != nil {
|
||||||
log.Fatal(4, "fail to create '%s': %v", SSHPath, err)
|
log.Fatal(4, "fail to create '%s': %v", SSHPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,17 +89,7 @@ func (k *PublicKey) OmitEmail() string {
|
||||||
|
|
||||||
// GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
|
// GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
|
||||||
func (key *PublicKey) GetAuthorizedString() string {
|
func (key *PublicKey) GetAuthorizedString() string {
|
||||||
return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.ID, setting.CustomConf, key.Content)
|
return fmt.Sprintf(_TPL_PUBLICK_KEY, setting.AppPath, key.ID, setting.CustomConf, key.Content)
|
||||||
}
|
|
||||||
|
|
||||||
var minimumKeySizes = map[string]int{
|
|
||||||
"(ED25519)": 256,
|
|
||||||
"(ECDSA)": 256,
|
|
||||||
"(NTRU)": 1087,
|
|
||||||
"(MCE)": 1702,
|
|
||||||
"(McE)": 1702,
|
|
||||||
"(RSA)": 1024,
|
|
||||||
"(DSA)": 1024,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractTypeFromBase64Key(key string) (string, error) {
|
func extractTypeFromBase64Key(key string) (string, error) {
|
||||||
|
@ -228,9 +193,9 @@ func CheckPublicKeyString(content string) (_ string, err error) {
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
// Check if ssh-keygen recognizes its contents.
|
// Check if ssh-keygen recognizes its contents.
|
||||||
stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath)
|
stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-lf", tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("ssh-keygen -l -f: " + stderr)
|
return "", errors.New("ssh-keygen -lf: " + stderr)
|
||||||
} else if len(stdout) < 2 {
|
} else if len(stdout) < 2 {
|
||||||
return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout)
|
return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout)
|
||||||
}
|
}
|
||||||
|
@ -242,7 +207,7 @@ func CheckPublicKeyString(content string) (_ string, err error) {
|
||||||
|
|
||||||
sshKeygenOutput := strings.Split(stdout, " ")
|
sshKeygenOutput := strings.Split(stdout, " ")
|
||||||
if len(sshKeygenOutput) < 4 {
|
if len(sshKeygenOutput) < 4 {
|
||||||
return content, ErrKeyUnableVerify
|
return content, ErrKeyUnableVerify{stdout}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if key type and key size match.
|
// Check if key type and key size match.
|
||||||
|
@ -251,9 +216,10 @@ func CheckPublicKeyString(content string) (_ string, err error) {
|
||||||
if keySize == 0 {
|
if keySize == 0 {
|
||||||
return "", errors.New("cannot get key size of the given key")
|
return "", errors.New("cannot get key size of the given key")
|
||||||
}
|
}
|
||||||
keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1])
|
|
||||||
if minimumKeySize := minimumKeySizes[keyType]; minimumKeySize == 0 {
|
keyType := strings.Trim(sshKeygenOutput[len(sshKeygenOutput)-1], " ()\n")
|
||||||
return "", errors.New("sorry, unrecognized public key type")
|
if minimumKeySize := setting.Service.MinimumKeySizes[keyType]; minimumKeySize == 0 {
|
||||||
|
return "", fmt.Errorf("unrecognized public key type: %s", keyType)
|
||||||
} else if keySize < minimumKeySize {
|
} else if keySize < minimumKeySize {
|
||||||
return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
|
return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
|
||||||
}
|
}
|
||||||
|
@ -321,9 +287,9 @@ func addKey(e Engine, key *PublicKey) (err error) {
|
||||||
if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil {
|
if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath)
|
stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("ssh-keygen -l -f: " + stderr)
|
return errors.New("ssh-keygen -lf: " + stderr)
|
||||||
} else if len(stdout) < 2 {
|
} else if len(stdout) < 2 {
|
||||||
return errors.New("not enough output for calculating fingerprint: " + stdout)
|
return errors.New("not enough output for calculating fingerprint: " + stdout)
|
||||||
}
|
}
|
||||||
|
@ -382,6 +348,19 @@ func GetPublicKeyByID(keyID int64) (*PublicKey, error) {
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchPublicKeyByContent searches content as prefix (leak e-mail part)
|
||||||
|
// and returns public key found.
|
||||||
|
func SearchPublicKeyByContent(content string) (*PublicKey, error) {
|
||||||
|
key := new(PublicKey)
|
||||||
|
has, err := x.Where("content like ?", content+"%").Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrKeyNotExist{}
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListPublicKeys returns a list of public keys belongs to given user.
|
// ListPublicKeys returns a list of public keys belongs to given user.
|
||||||
func ListPublicKeys(uid int64) ([]*PublicKey, error) {
|
func ListPublicKeys(uid int64) ([]*PublicKey, error) {
|
||||||
keys := make([]*PublicKey, 0, 5)
|
keys := make([]*PublicKey, 0, 5)
|
||||||
|
@ -540,6 +519,7 @@ type DeployKey struct {
|
||||||
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
Name string
|
Name string
|
||||||
Fingerprint string
|
Fingerprint string
|
||||||
|
Content string `xorm:"-"`
|
||||||
Created time.Time `xorm:"CREATED"`
|
Created time.Time `xorm:"CREATED"`
|
||||||
Updated time.Time // Note: Updated must below Created for AfterSet.
|
Updated time.Time // Note: Updated must below Created for AfterSet.
|
||||||
HasRecentActivity bool `xorm:"-"`
|
HasRecentActivity bool `xorm:"-"`
|
||||||
|
@ -554,6 +534,16 @@ func (k *DeployKey) AfterSet(colName string, _ xorm.Cell) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContent gets associated public key content.
|
||||||
|
func (k *DeployKey) GetContent() error {
|
||||||
|
pkey, err := GetPublicKeyByID(k.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
k.Content = pkey.Content
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
|
func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
|
||||||
// Note: We want error detail, not just true or false here.
|
// Note: We want error detail, not just true or false here.
|
||||||
has, err := e.Where("key_id=? AND repo_id=?", keyID, repoID).Get(new(DeployKey))
|
has, err := e.Where("key_id=? AND repo_id=?", keyID, repoID).Get(new(DeployKey))
|
||||||
|
@ -574,18 +564,19 @@ func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// addDeployKey adds new key-repo relation.
|
// addDeployKey adds new key-repo relation.
|
||||||
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string) (err error) {
|
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string) (*DeployKey, error) {
|
||||||
if err = checkDeployKey(e, keyID, repoID, name); err != nil {
|
if err := checkDeployKey(e, keyID, repoID, name); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = e.Insert(&DeployKey{
|
key := &DeployKey{
|
||||||
KeyID: keyID,
|
KeyID: keyID,
|
||||||
RepoID: repoID,
|
RepoID: repoID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Fingerprint: fingerprint,
|
Fingerprint: fingerprint,
|
||||||
})
|
}
|
||||||
return err
|
_, err := e.Insert(key)
|
||||||
|
return key, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasDeployKey returns true if public key is a deploy key of given repository.
|
// HasDeployKey returns true if public key is a deploy key of given repository.
|
||||||
|
@ -595,39 +586,52 @@ func HasDeployKey(keyID, repoID int64) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDeployKey add new deploy key to database and authorized_keys file.
|
// AddDeployKey add new deploy key to database and authorized_keys file.
|
||||||
func AddDeployKey(repoID int64, name, content string) (err error) {
|
func AddDeployKey(repoID int64, name, content string) (*DeployKey, error) {
|
||||||
if err = checkKeyContent(content); err != nil {
|
if err := checkKeyContent(content); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
key := &PublicKey{
|
pkey := &PublicKey{
|
||||||
Content: content,
|
Content: content,
|
||||||
Mode: ACCESS_MODE_READ,
|
Mode: ACCESS_MODE_READ,
|
||||||
Type: KEY_TYPE_DEPLOY,
|
Type: KEY_TYPE_DEPLOY,
|
||||||
}
|
}
|
||||||
has, err := x.Get(key)
|
has, err := x.Get(pkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := x.NewSession()
|
sess := x.NewSession()
|
||||||
defer sessionRelease(sess)
|
defer sessionRelease(sess)
|
||||||
if err = sess.Begin(); err != nil {
|
if err = sess.Begin(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// First time use this deploy key.
|
// First time use this deploy key.
|
||||||
if !has {
|
if !has {
|
||||||
if err = addKey(sess, key); err != nil {
|
if err = addKey(sess, pkey); err != nil {
|
||||||
return nil
|
return nil, fmt.Errorf("addKey: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = addDeployKey(sess, key.ID, repoID, name, key.Fingerprint); err != nil {
|
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("addDeployKey: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess.Commit()
|
return key, sess.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeployKeyByID returns deploy key by given ID.
|
||||||
|
func GetDeployKeyByID(id int64) (*DeployKey, error) {
|
||||||
|
key := new(DeployKey)
|
||||||
|
has, err := x.Id(id).Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrDeployKeyNotExist{id, 0, 0}
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
|
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
|
||||||
|
@ -636,8 +640,13 @@ func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
|
||||||
KeyID: keyID,
|
KeyID: keyID,
|
||||||
RepoID: repoID,
|
RepoID: repoID,
|
||||||
}
|
}
|
||||||
_, err := x.Get(key)
|
has, err := x.Get(key)
|
||||||
return key, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrDeployKeyNotExist{0, keyID, repoID}
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDeployKey updates deploy key information.
|
// UpdateDeployKey updates deploy key information.
|
||||||
|
|
|
@ -192,12 +192,18 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
|
||||||
return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
|
return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
|
||||||
|
fmt.Sprintf("PullRequest.Merge (git merge --no-ff --no-commit): %s", tmpBasePath),
|
||||||
|
"git", "merge", "--no-ff", "--no-commit", "head_repo/"+pr.HeadBranch); err != nil {
|
||||||
|
return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := doer.NewGitSig()
|
||||||
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
|
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
|
||||||
fmt.Sprintf("PullRequest.Merge (git merge): %s", tmpBasePath),
|
fmt.Sprintf("PullRequest.Merge (git merge): %s", tmpBasePath),
|
||||||
"git", "merge", "--no-ff", "-m",
|
"git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
|
||||||
fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch),
|
"-m", fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch)); err != nil {
|
||||||
"head_repo/"+pr.HeadBranch); err != nil {
|
return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, stderr)
|
||||||
return fmt.Errorf("git merge[%s]: %s", tmpBasePath, stderr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push back to upstream.
|
// Push back to upstream.
|
||||||
|
@ -218,6 +224,7 @@ var patchConflicts = []string{
|
||||||
}
|
}
|
||||||
|
|
||||||
// testPatch checks if patch can be merged to base repository without conflit.
|
// testPatch checks if patch can be merged to base repository without conflit.
|
||||||
|
// FIXME: make a mechanism to clean up stable local copies.
|
||||||
func (pr *PullRequest) testPatch() (err error) {
|
func (pr *PullRequest) testPatch() (err error) {
|
||||||
if pr.BaseRepo == nil {
|
if pr.BaseRepo == nil {
|
||||||
pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID)
|
pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID)
|
||||||
|
@ -243,14 +250,23 @@ func (pr *PullRequest) testPatch() (err error) {
|
||||||
return fmt.Errorf("UpdateLocalCopy: %v", err)
|
return fmt.Errorf("UpdateLocalCopy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.Status = PULL_REQUEST_STATUS_CHECKING
|
// Checkout base branch.
|
||||||
_, stderr, err := process.ExecDir(-1, pr.BaseRepo.LocalCopyPath(),
|
_, stderr, err := process.ExecDir(-1, pr.BaseRepo.LocalCopyPath(),
|
||||||
|
fmt.Sprintf("PullRequest.Merge(git checkout): %s", pr.BaseRepo.ID),
|
||||||
|
"git", "checkout", pr.BaseBranch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git checkout: %s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.Status = PULL_REQUEST_STATUS_CHECKING
|
||||||
|
_, stderr, err = process.ExecDir(-1, pr.BaseRepo.LocalCopyPath(),
|
||||||
fmt.Sprintf("testPatch(git apply --check): %d", pr.BaseRepo.ID),
|
fmt.Sprintf("testPatch(git apply --check): %d", pr.BaseRepo.ID),
|
||||||
"git", "apply", "--check", patchPath)
|
"git", "apply", "--check", patchPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for i := range patchConflicts {
|
for i := range patchConflicts {
|
||||||
if strings.Contains(stderr, patchConflicts[i]) {
|
if strings.Contains(stderr, patchConflicts[i]) {
|
||||||
log.Trace("PullRequest[%d].testPatch(apply): has conflit", pr.ID)
|
log.Trace("PullRequest[%d].testPatch(apply): has conflit", pr.ID)
|
||||||
|
fmt.Println(stderr)
|
||||||
pr.Status = PULL_REQUEST_STATUS_CONFLICT
|
pr.Status = PULL_REQUEST_STATUS_CONFLICT
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -385,16 +401,18 @@ func (pr *PullRequest) UpdateCols(cols ...string) error {
|
||||||
var PullRequestQueue = NewUniqueQueue(setting.Repository.PullRequestQueueLength)
|
var PullRequestQueue = NewUniqueQueue(setting.Repository.PullRequestQueueLength)
|
||||||
|
|
||||||
// UpdatePatch generates and saves a new patch.
|
// UpdatePatch generates and saves a new patch.
|
||||||
func (pr *PullRequest) UpdatePatch() error {
|
func (pr *PullRequest) UpdatePatch() (err error) {
|
||||||
if err := pr.GetHeadRepo(); err != nil {
|
if err = pr.GetHeadRepo(); err != nil {
|
||||||
return fmt.Errorf("GetHeadRepo: %v", err)
|
return fmt.Errorf("GetHeadRepo: %v", err)
|
||||||
} else if pr.HeadRepo == nil {
|
} else if pr.HeadRepo == nil {
|
||||||
log.Trace("PullRequest[%d].UpdatePatch: ignored cruppted data", pr.ID)
|
log.Trace("PullRequest[%d].UpdatePatch: ignored cruppted data", pr.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pr.GetBaseRepo(); err != nil {
|
if err = pr.GetBaseRepo(); err != nil {
|
||||||
return fmt.Errorf("GetBaseRepo: %v", err)
|
return fmt.Errorf("GetBaseRepo: %v", err)
|
||||||
|
} else if err = pr.BaseRepo.GetOwner(); err != nil {
|
||||||
|
return fmt.Errorf("GetOwner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
headRepoPath, err := pr.HeadRepo.RepoPath()
|
headRepoPath, err := pr.HeadRepo.RepoPath()
|
||||||
|
@ -407,6 +425,22 @@ func (pr *PullRequest) UpdatePatch() error {
|
||||||
return fmt.Errorf("OpenRepository: %v", err)
|
return fmt.Errorf("OpenRepository: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a temporary remote.
|
||||||
|
tmpRemote := com.ToStr(time.Now().UnixNano())
|
||||||
|
if err = headGitRepo.AddRemote(tmpRemote, RepoPath(pr.BaseRepo.Owner.Name, pr.BaseRepo.Name)); err != nil {
|
||||||
|
return fmt.Errorf("AddRemote: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
headGitRepo.RemoveRemote(tmpRemote)
|
||||||
|
}()
|
||||||
|
remoteBranch := "remotes/" + tmpRemote + "/" + pr.BaseBranch
|
||||||
|
pr.MergeBase, err = headGitRepo.GetMergeBase(remoteBranch, pr.HeadBranch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetMergeBase: %v", err)
|
||||||
|
} else if err = pr.Update(); err != nil {
|
||||||
|
return fmt.Errorf("Update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
patch, err := headGitRepo.GetPatch(pr.MergeBase, pr.HeadBranch)
|
patch, err := headGitRepo.GetPatch(pr.MergeBase, pr.HeadBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetPatch: %v", err)
|
return fmt.Errorf("GetPatch: %v", err)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -13,18 +13,14 @@ import (
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
|
||||||
"github.com/gogits/gogs/modules/git"
|
"github.com/gogits/gogs/modules/git"
|
||||||
)
|
"github.com/gogits/gogs/modules/process"
|
||||||
|
|
||||||
var (
|
|
||||||
ErrReleaseAlreadyExist = errors.New("Release already exist")
|
|
||||||
ErrReleaseNotExist = errors.New("Release does not exist")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Release represents a release of repository.
|
// Release represents a release of repository.
|
||||||
type Release struct {
|
type Release struct {
|
||||||
Id int64
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RepoId int64
|
RepoID int64
|
||||||
PublisherId int64
|
PublisherID int64
|
||||||
Publisher *User `xorm:"-"`
|
Publisher *User `xorm:"-"`
|
||||||
TagName string
|
TagName string
|
||||||
LowerTagName string
|
LowerTagName string
|
||||||
|
@ -47,12 +43,12 @@ func (r *Release) AfterSet(colName string, _ xorm.Cell) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsReleaseExist returns true if release with given tag name already exists.
|
// IsReleaseExist returns true if release with given tag name already exists.
|
||||||
func IsReleaseExist(repoId int64, tagName string) (bool, error) {
|
func IsReleaseExist(repoID int64, tagName string) (bool, error) {
|
||||||
if len(tagName) == 0 {
|
if len(tagName) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return x.Get(&Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)})
|
return x.Get(&Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTag(gitRepo *git.Repository, rel *Release) error {
|
func createTag(gitRepo *git.Repository, rel *Release) error {
|
||||||
|
@ -64,7 +60,7 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = gitRepo.CreateTag(rel.TagName, commit.Id.String()); err != nil {
|
if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,11 +80,11 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
|
||||||
|
|
||||||
// CreateRelease creates a new release of repository.
|
// CreateRelease creates a new release of repository.
|
||||||
func CreateRelease(gitRepo *git.Repository, rel *Release) error {
|
func CreateRelease(gitRepo *git.Repository, rel *Release) error {
|
||||||
isExist, err := IsReleaseExist(rel.RepoId, rel.TagName)
|
isExist, err := IsReleaseExist(rel.RepoID, rel.TagName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if isExist {
|
} else if isExist {
|
||||||
return ErrReleaseAlreadyExist
|
return ErrReleaseAlreadyExist{rel.TagName}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = createTag(gitRepo, rel); err != nil {
|
if err = createTag(gitRepo, rel); err != nil {
|
||||||
|
@ -100,22 +96,35 @@ func CreateRelease(gitRepo *git.Repository, rel *Release) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRelease returns release by given ID.
|
// GetRelease returns release by given ID.
|
||||||
func GetRelease(repoId int64, tagName string) (*Release, error) {
|
func GetRelease(repoID int64, tagName string) (*Release, error) {
|
||||||
isExist, err := IsReleaseExist(repoId, tagName)
|
isExist, err := IsReleaseExist(repoID, tagName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !isExist {
|
} else if !isExist {
|
||||||
return nil, ErrReleaseNotExist
|
return nil, ErrReleaseNotExist{0, tagName}
|
||||||
}
|
}
|
||||||
|
|
||||||
rel := &Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)}
|
rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
|
||||||
_, err = x.Get(rel)
|
_, err = x.Get(rel)
|
||||||
return rel, err
|
return rel, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReleasesByRepoId returns a list of releases of repository.
|
// GetReleaseByID returns release with given ID.
|
||||||
func GetReleasesByRepoId(repoId int64) (rels []*Release, err error) {
|
func GetReleaseByID(id int64) (*Release, error) {
|
||||||
err = x.Desc("created").Find(&rels, Release{RepoId: repoId})
|
rel := new(Release)
|
||||||
|
has, err := x.Id(id).Get(rel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrReleaseNotExist{id, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReleasesByRepoID returns a list of releases of repository.
|
||||||
|
func GetReleasesByRepoID(repoID int64) (rels []*Release, err error) {
|
||||||
|
err = x.Desc("created").Find(&rels, Release{RepoID: repoID})
|
||||||
return rels, err
|
return rels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +159,36 @@ func UpdateRelease(gitRepo *git.Repository, rel *Release) (err error) {
|
||||||
if err = createTag(gitRepo, rel); err != nil {
|
if err = createTag(gitRepo, rel); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = x.Id(rel.Id).AllCols().Update(rel)
|
_, err = x.Id(rel.ID).AllCols().Update(rel)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteReleaseByID deletes a release and corresponding Git tag by given ID.
|
||||||
|
func DeleteReleaseByID(id int64) error {
|
||||||
|
rel, err := GetReleaseByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetReleaseByID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := GetRepositoryByID(rel.RepoID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetRepositoryByID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repoPath, err := repo.RepoPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("RepoPath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, stderr, err := process.ExecDir(-1, repoPath, fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID),
|
||||||
|
"git", "tag", "-d", rel.TagName)
|
||||||
|
if err != nil && !strings.Contains(stderr, "not found") {
|
||||||
|
return fmt.Errorf("git tag -d: %v - %s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
|
||||||
|
return fmt.Errorf("Delete: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
187
models/repo.go
187
models/repo.go
|
@ -17,12 +17,14 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/Unknwon/cae/zip"
|
"github.com/Unknwon/cae/zip"
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
|
||||||
"github.com/gogits/gogs/modules/base"
|
"github.com/gogits/gogs/modules/base"
|
||||||
"github.com/gogits/gogs/modules/bindata"
|
"github.com/gogits/gogs/modules/bindata"
|
||||||
|
@ -48,7 +50,7 @@ var (
|
||||||
Gitignores, Licenses, Readmes []string
|
Gitignores, Licenses, Readmes []string
|
||||||
|
|
||||||
// Maximum items per page in forks, watchers and stars of a repo
|
// Maximum items per page in forks, watchers and stars of a repo
|
||||||
ItemsPerPage = 54
|
ItemsPerPage = 40
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadRepoConfig() {
|
func LoadRepoConfig() {
|
||||||
|
@ -319,7 +321,7 @@ func (repo *Repository) UpdateLocalCopy() error {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, stderr, err := process.ExecDir(-1, localPath,
|
_, stderr, err := process.ExecDir(-1, localPath,
|
||||||
fmt.Sprintf("UpdateLocalCopy(git pull): %s", repoPath), "git", "pull")
|
fmt.Sprintf("UpdateLocalCopy(git pull --all): %s", repoPath), "git", "pull", "--all")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("git pull: %v - %s", err, stderr)
|
return fmt.Errorf("git pull: %v - %s", err, stderr)
|
||||||
}
|
}
|
||||||
|
@ -379,11 +381,11 @@ func (repo *Repository) CloneLink() (cl CloneLink, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.SSHPort != 22 {
|
if setting.SSHPort != 22 {
|
||||||
cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSHDomain, setting.SSHPort, repo.Owner.LowerName, repo.LowerName)
|
cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSHDomain, setting.SSHPort, repo.Owner.Name, repo.Name)
|
||||||
} else {
|
} else {
|
||||||
cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSHDomain, repo.Owner.LowerName, repo.LowerName)
|
cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSHDomain, repo.Owner.Name, repo.Name)
|
||||||
}
|
}
|
||||||
cl.HTTPS = fmt.Sprintf("%s%s/%s.git", setting.AppUrl, repo.Owner.LowerName, repo.LowerName)
|
cl.HTTPS = fmt.Sprintf("%s%s/%s.git", setting.AppUrl, repo.Owner.Name, repo.Name)
|
||||||
return cl, nil
|
return cl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -537,6 +539,17 @@ func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
|
||||||
return repo, fmt.Errorf("create update hook: %v", err)
|
return repo, fmt.Errorf("create update hook: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up mirror info which prevents "push --all".
|
||||||
|
configPath := filepath.Join(repoPath, "/config")
|
||||||
|
cfg, err := ini.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return repo, fmt.Errorf("open config file: %v", err)
|
||||||
|
}
|
||||||
|
cfg.DeleteSection("remote \"origin\"")
|
||||||
|
if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
|
||||||
|
return repo, fmt.Errorf("save config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if repository is empty.
|
// Check if repository is empty.
|
||||||
_, stderr, err = com.ExecCmdDir(repoPath, "git", "log", "-1")
|
_, stderr, err = com.ExecCmdDir(repoPath, "git", "log", "-1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -587,7 +600,7 @@ func createUpdateHook(repoPath string) error {
|
||||||
hookPath := path.Join(repoPath, "hooks/update")
|
hookPath := path.Join(repoPath, "hooks/update")
|
||||||
os.MkdirAll(path.Dir(hookPath), os.ModePerm)
|
os.MkdirAll(path.Dir(hookPath), os.ModePerm)
|
||||||
return ioutil.WriteFile(hookPath,
|
return ioutil.WriteFile(hookPath,
|
||||||
[]byte(fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+appPath+"\"", setting.CustomConf)), 0777)
|
[]byte(fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+setting.AppPath+"\"", setting.CustomConf)), 0777)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateRepoOptions struct {
|
type CreateRepoOptions struct {
|
||||||
|
@ -898,9 +911,9 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove redundant collaborators.
|
// Remove redundant collaborators.
|
||||||
collaborators, err := repo.GetCollaborators()
|
collaborators, err := repo.getCollaborators(sess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetCollaborators: %v", err)
|
return fmt.Errorf("getCollaborators: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy object.
|
// Dummy object.
|
||||||
|
@ -936,9 +949,9 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if newOwner.IsOrganization() {
|
if newOwner.IsOrganization() {
|
||||||
t, err := newOwner.GetOwnerTeam()
|
t, err := newOwner.getOwnerTeam(sess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetOwnerTeam: %v", err)
|
return fmt.Errorf("getOwnerTeam: %v", err)
|
||||||
} else if err = t.addRepository(sess, repo); err != nil {
|
} else if err = t.addRepository(sess, repo); err != nil {
|
||||||
return fmt.Errorf("add to owner team: %v", err)
|
return fmt.Errorf("add to owner team: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1104,7 +1117,7 @@ func DeleteRepository(uid, repoID int64) error {
|
||||||
return err
|
return err
|
||||||
} else if _, err = sess.Delete(&Milestone{RepoID: repoID}); err != nil {
|
} else if _, err = sess.Delete(&Milestone{RepoID: repoID}); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if _, err = sess.Delete(&Release{RepoId: repoID}); err != nil {
|
} else if _, err = sess.Delete(&Release{RepoID: repoID}); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if _, err = sess.Delete(&Collaboration{RepoID: repoID}); err != nil {
|
} else if _, err = sess.Delete(&Collaboration{RepoID: repoID}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1304,7 +1317,7 @@ func DeleteRepositoryArchives() error {
|
||||||
repo := bean.(*Repository)
|
repo := bean.(*Repository)
|
||||||
repoPath, err := repo.RepoPath()
|
repoPath, err := repo.RepoPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepositoryArchives[%d]: %v", repo.ID, err)); err2 != nil {
|
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepositoryArchives.RepoPath [%d]: %v", repo.ID, err)); err2 != nil {
|
||||||
log.Error(4, "CreateRepositoryNotice: %v", err2)
|
log.Error(4, "CreateRepositoryNotice: %v", err2)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -1313,32 +1326,110 @@ func DeleteRepositoryArchives() error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteMissingRepositories deletes all repository records that lost Git files.
|
||||||
|
func DeleteMissingRepositories() error {
|
||||||
|
repos := make([]*Repository, 0, 5)
|
||||||
|
if err := x.Where("id > 0").Iterate(new(Repository),
|
||||||
|
func(idx int, bean interface{}) error {
|
||||||
|
repo := bean.(*Repository)
|
||||||
|
repoPath, err := repo.RepoPath()
|
||||||
|
if err != nil {
|
||||||
|
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepositoryArchives.RepoPath [%d]: %v", repo.ID, err)); err2 != nil {
|
||||||
|
log.Error(4, "CreateRepositoryNotice: %v", err2)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !com.IsDir(repoPath) {
|
||||||
|
repos = append(repos, repo)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteMissingRepositories: %v", err)); err2 != nil {
|
||||||
|
log.Error(4, "CreateRepositoryNotice: %v", err2)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repos) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repo := range repos {
|
||||||
|
log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID)
|
||||||
|
if err := DeleteRepository(repo.OwnerID, repo.ID); err != nil {
|
||||||
|
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil {
|
||||||
|
log.Error(4, "CreateRepositoryNotice: %v", err2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RewriteRepositoryUpdateHook rewrites all repositories' update hook.
|
// RewriteRepositoryUpdateHook rewrites all repositories' update hook.
|
||||||
func RewriteRepositoryUpdateHook() error {
|
func RewriteRepositoryUpdateHook() error {
|
||||||
return x.Where("id > 0").Iterate(new(Repository),
|
return x.Where("id > 0").Iterate(new(Repository),
|
||||||
func(idx int, bean interface{}) error {
|
func(idx int, bean interface{}) error {
|
||||||
repo := bean.(*Repository)
|
repo := bean.(*Repository)
|
||||||
if err := repo.GetOwner(); err != nil {
|
repoPath, err := repo.RepoPath()
|
||||||
return err
|
if err != nil {
|
||||||
|
if err2 := CreateRepositoryNotice(fmt.Sprintf("RewriteRepositoryUpdateHook[%d]: %v", repo.ID, err)); err2 != nil {
|
||||||
|
log.Error(4, "CreateRepositoryNotice: %v", err2)
|
||||||
}
|
}
|
||||||
return createUpdateHook(RepoPath(repo.Owner.Name, repo.Name))
|
return nil
|
||||||
|
}
|
||||||
|
return createUpdateHook(repoPath)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// statusPool represents a pool of status with true/false.
|
||||||
|
type statusPool struct {
|
||||||
|
lock sync.RWMutex
|
||||||
|
pool map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start sets value of given name to true in the pool.
|
||||||
|
func (p *statusPool) Start(name string) {
|
||||||
|
p.lock.Lock()
|
||||||
|
defer p.lock.Unlock()
|
||||||
|
|
||||||
|
p.pool[name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop sets value of given name to false in the pool.
|
||||||
|
func (p *statusPool) Stop(name string) {
|
||||||
|
p.lock.Lock()
|
||||||
|
defer p.lock.Unlock()
|
||||||
|
|
||||||
|
p.pool[name] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRunning checks if value of given name is set to true in the pool.
|
||||||
|
func (p *statusPool) IsRunning(name string) bool {
|
||||||
|
p.lock.RLock()
|
||||||
|
defer p.lock.RUnlock()
|
||||||
|
|
||||||
|
return p.pool[name]
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent duplicate running tasks.
|
// Prevent duplicate running tasks.
|
||||||
isMirrorUpdating = false
|
var taskStatusPool = &statusPool{
|
||||||
isGitFscking = false
|
pool: make(map[string]bool),
|
||||||
isCheckingRepos = false
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_MIRROR_UPDATE = "mirror_update"
|
||||||
|
_GIT_FSCK = "git_fsck"
|
||||||
|
_CHECK_REPOs = "check_repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MirrorUpdate checks and updates mirror repositories.
|
// MirrorUpdate checks and updates mirror repositories.
|
||||||
func MirrorUpdate() {
|
func MirrorUpdate() {
|
||||||
if isMirrorUpdating {
|
if taskStatusPool.IsRunning(_MIRROR_UPDATE) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isMirrorUpdating = true
|
taskStatusPool.Start(_MIRROR_UPDATE)
|
||||||
defer func() { isMirrorUpdating = false }()
|
defer taskStatusPool.Stop(_MIRROR_UPDATE)
|
||||||
|
|
||||||
log.Trace("Doing: MirrorUpdate")
|
log.Trace("Doing: MirrorUpdate")
|
||||||
|
|
||||||
|
@ -1386,11 +1477,11 @@ func MirrorUpdate() {
|
||||||
|
|
||||||
// GitFsck calls 'git fsck' to check repository health.
|
// GitFsck calls 'git fsck' to check repository health.
|
||||||
func GitFsck() {
|
func GitFsck() {
|
||||||
if isGitFscking {
|
if taskStatusPool.IsRunning(_GIT_FSCK) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isGitFscking = true
|
taskStatusPool.Start(_GIT_FSCK)
|
||||||
defer func() { isGitFscking = false }()
|
defer taskStatusPool.Stop(_GIT_FSCK)
|
||||||
|
|
||||||
log.Trace("Doing: GitFsck")
|
log.Trace("Doing: GitFsck")
|
||||||
|
|
||||||
|
@ -1455,11 +1546,11 @@ func repoStatsCheck(checker *repoChecker) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckRepoStats() {
|
func CheckRepoStats() {
|
||||||
if isCheckingRepos {
|
if taskStatusPool.IsRunning(_CHECK_REPOs) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCheckingRepos = true
|
taskStatusPool.Start(_CHECK_REPOs)
|
||||||
defer func() { isCheckingRepos = false }()
|
defer taskStatusPool.Stop(_CHECK_REPOs)
|
||||||
|
|
||||||
log.Trace("Doing: CheckRepoStats")
|
log.Trace("Doing: CheckRepoStats")
|
||||||
|
|
||||||
|
@ -1680,25 +1771,21 @@ func WatchRepo(uid, repoId int64, watch bool) (err error) {
|
||||||
return watchRepo(x, uid, repoId, watch)
|
return watchRepo(x, uid, repoId, watch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWatchers(e Engine, rid int64) ([]*Watch, error) {
|
func getWatchers(e Engine, repoID int64) ([]*Watch, error) {
|
||||||
watches := make([]*Watch, 0, 10)
|
watches := make([]*Watch, 0, 10)
|
||||||
err := e.Find(&watches, &Watch{RepoID: rid})
|
return watches, e.Find(&watches, &Watch{RepoID: repoID})
|
||||||
return watches, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWatchers returns all watchers of given repository.
|
// GetWatchers returns all watchers of given repository.
|
||||||
func GetWatchers(rid int64) ([]*Watch, error) {
|
func GetWatchers(repoID int64) ([]*Watch, error) {
|
||||||
return getWatchers(x, rid)
|
return getWatchers(x, repoID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repository.GetWatchers returns all users watching given repository.
|
// Repository.GetWatchers returns range of users watching given repository.
|
||||||
func (repo *Repository) GetWatchers(offset int) ([]*User, error) {
|
func (repo *Repository) GetWatchers(page int) ([]*User, error) {
|
||||||
users := make([]*User, 0, 10)
|
users := make([]*User, 0, ItemsPerPage)
|
||||||
offset = (offset - 1) * ItemsPerPage
|
return users, x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).
|
||||||
|
Where("repo_id=?", repo.ID).Join("LEFT", "watch", "user.id=watch.user_id").Find(&users)
|
||||||
err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "watch", "user.id=watch.user_id").Find(&users)
|
|
||||||
|
|
||||||
return users, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyWatchers(e Engine, act *Action) error {
|
func notifyWatchers(e Engine, act *Action) error {
|
||||||
|
@ -1778,13 +1865,10 @@ func IsStaring(uid, repoId int64) bool {
|
||||||
return has
|
return has
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *Repository) GetStars(offset int) ([]*User, error) {
|
func (repo *Repository) GetStargazers(page int) ([]*User, error) {
|
||||||
users := make([]*User, 0, 10)
|
users := make([]*User, 0, ItemsPerPage)
|
||||||
offset = (offset - 1) * ItemsPerPage
|
return users, x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).
|
||||||
|
Where("repo_id=?", repo.ID).Join("LEFT", "star", "user.id=star.uid").Find(&users)
|
||||||
err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "star", "user.id=star.uid").Find(&users)
|
|
||||||
|
|
||||||
return users, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ___________ __
|
// ___________ __
|
||||||
|
@ -1856,9 +1940,6 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *Repository) GetForks() ([]*Repository, error) {
|
func (repo *Repository) GetForks() ([]*Repository, error) {
|
||||||
forks := make([]*Repository, 0, 10)
|
forks := make([]*Repository, 0, repo.NumForks)
|
||||||
|
return forks, x.Find(&forks, &Repository{ForkID: repo.ID})
|
||||||
err := x.Find(&forks, &Repository{ForkID: repo.ID})
|
|
||||||
|
|
||||||
return forks, err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gogits/gogs/modules/base"
|
|
||||||
"github.com/gogits/gogs/modules/git"
|
"github.com/gogits/gogs/modules/git"
|
||||||
"github.com/gogits/gogs/modules/log"
|
"github.com/gogits/gogs/modules/log"
|
||||||
)
|
)
|
||||||
|
@ -28,6 +27,7 @@ func AddUpdateTask(task *UpdateTask) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUpdateTaskByUUID returns update task by given UUID.
|
||||||
func GetUpdateTaskByUUID(uuid string) (*UpdateTask, error) {
|
func GetUpdateTaskByUUID(uuid string) (*UpdateTask, error) {
|
||||||
task := &UpdateTask{
|
task := &UpdateTask{
|
||||||
UUID: uuid,
|
UUID: uuid,
|
||||||
|
@ -36,7 +36,7 @@ func GetUpdateTaskByUUID(uuid string) (*UpdateTask, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, fmt.Errorf("task does not exist: %s", uuid)
|
return nil, ErrUpdateTaskNotExist{uuid}
|
||||||
}
|
}
|
||||||
return task, nil
|
return task, nil
|
||||||
}
|
}
|
||||||
|
@ -46,10 +46,10 @@ func DeleteUpdateTaskByUUID(uuid string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName string, userId int64) error {
|
func Update(refName, oldCommitID, newCommitID, userName, repoUserName, repoName string, userID int64) error {
|
||||||
isNew := strings.HasPrefix(oldCommitId, "0000000")
|
isNew := strings.HasPrefix(oldCommitID, "0000000")
|
||||||
if isNew &&
|
if isNew &&
|
||||||
strings.HasPrefix(newCommitId, "0000000") {
|
strings.HasPrefix(newCommitID, "0000000") {
|
||||||
return fmt.Errorf("old rev and new rev both 000000")
|
return fmt.Errorf("old rev and new rev both 000000")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,23 +59,23 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
|
||||||
gitUpdate.Dir = f
|
gitUpdate.Dir = f
|
||||||
gitUpdate.Run()
|
gitUpdate.Run()
|
||||||
|
|
||||||
isDel := strings.HasPrefix(newCommitId, "0000000")
|
isDel := strings.HasPrefix(newCommitID, "0000000")
|
||||||
if isDel {
|
if isDel {
|
||||||
log.GitLogger.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userId)
|
log.GitLogger.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := git.OpenRepository(f)
|
gitRepo, err := git.OpenRepository(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("runUpdate.Open repoId: %v", err)
|
return fmt.Errorf("runUpdate.Open repoId: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ru, err := GetUserByName(repoUserName)
|
user, err := GetUserByName(repoUserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("runUpdate.GetUserByName: %v", err)
|
return fmt.Errorf("runUpdate.GetUserByName: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
repos, err := GetRepositoryByName(ru.Id, repoName)
|
repo, err := GetRepositoryByName(user.Id, repoName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("runUpdate.GetRepositoryByName userId: %v", err)
|
return fmt.Errorf("runUpdate.GetRepositoryByName userId: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
|
||||||
// Push tags.
|
// Push tags.
|
||||||
if strings.HasPrefix(refName, "refs/tags/") {
|
if strings.HasPrefix(refName, "refs/tags/") {
|
||||||
tagName := git.RefEndName(refName)
|
tagName := git.RefEndName(refName)
|
||||||
tag, err := repo.GetTag(tagName)
|
tag, err := gitRepo.GetTag(tagName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.GitLogger.Fatal(4, "runUpdate.GetTag: %v", err)
|
log.GitLogger.Fatal(4, "runUpdate.GetTag: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -99,16 +99,16 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
|
||||||
actEmail = cmt.Committer.Email
|
actEmail = cmt.Committer.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
commit := &base.PushCommits{}
|
commit := &PushCommits{}
|
||||||
|
|
||||||
if err = CommitRepoAction(userId, ru.Id, userName, actEmail,
|
if err = CommitRepoAction(userID, user.Id, userName, actEmail,
|
||||||
repos.ID, repoUserName, repoName, refName, commit, oldCommitId, newCommitId); err != nil {
|
repo.ID, repoUserName, repoName, refName, commit, oldCommitID, newCommitID); err != nil {
|
||||||
log.GitLogger.Fatal(4, "CommitRepoAction: %s/%s:%v", repoUserName, repoName, err)
|
log.GitLogger.Fatal(4, "CommitRepoAction: %s/%s:%v", repoUserName, repoName, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newCommit, err := repo.GetCommit(newCommitId)
|
newCommit, err := gitRepo.GetCommit(newCommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("runUpdate GetCommit of newCommitId: %v", err)
|
return fmt.Errorf("runUpdate GetCommit of newCommitId: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
|
||||||
return fmt.Errorf("CommitsBefore: %v", err)
|
return fmt.Errorf("CommitsBefore: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
l, err = newCommit.CommitsBeforeUntil(oldCommitId)
|
l, err = newCommit.CommitsBeforeUntil(oldCommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CommitsBeforeUntil: %v", err)
|
return fmt.Errorf("CommitsBeforeUntil: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push commits.
|
// Push commits.
|
||||||
commits := make([]*base.PushCommit, 0)
|
commits := make([]*PushCommit, 0)
|
||||||
var actEmail string
|
var actEmail string
|
||||||
for e := l.Front(); e != nil; e = e.Next() {
|
for e := l.Front(); e != nil; e = e.Next() {
|
||||||
commit := e.Value.(*git.Commit)
|
commit := e.Value.(*git.Commit)
|
||||||
|
@ -140,15 +140,15 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
|
||||||
actEmail = commit.Committer.Email
|
actEmail = commit.Committer.Email
|
||||||
}
|
}
|
||||||
commits = append(commits,
|
commits = append(commits,
|
||||||
&base.PushCommit{commit.Id.String(),
|
&PushCommit{commit.ID.String(),
|
||||||
commit.Message(),
|
commit.Message(),
|
||||||
commit.Author.Email,
|
commit.Author.Email,
|
||||||
commit.Author.Name,
|
commit.Author.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = CommitRepoAction(userId, ru.Id, userName, actEmail,
|
if err = CommitRepoAction(userID, user.Id, userName, actEmail,
|
||||||
repos.ID, repoUserName, repoName, refName, &base.PushCommits{l.Len(), commits, ""}, oldCommitId, newCommitId); err != nil {
|
repo.ID, repoUserName, repoName, refName, &PushCommits{l.Len(), commits, "", nil}, oldCommitID, newCommitID); err != nil {
|
||||||
return fmt.Errorf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err)
|
return fmt.Errorf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
|
"image/png"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -78,6 +79,7 @@ type User struct {
|
||||||
IsActive bool
|
IsActive bool
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
AllowGitHook bool
|
AllowGitHook bool
|
||||||
|
AllowImportLocal bool // Allow migrate repository by local path
|
||||||
|
|
||||||
// Avatar.
|
// Avatar.
|
||||||
Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
|
Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
|
||||||
|
@ -107,6 +109,22 @@ func (u *User) AfterSet(colName string, _ xorm.Cell) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasForkedRepo checks if user has already forked a repository with given ID.
|
||||||
|
func (u *User) HasForkedRepo(repoID int64) bool {
|
||||||
|
_, has := HasForkedRepo(u.Id, repoID)
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanEditGitHook returns true if user can edit Git hooks.
|
||||||
|
func (u *User) CanEditGitHook() bool {
|
||||||
|
return u.IsAdmin || u.AllowGitHook
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanImportLocal returns true if user can migrate repository by local path.
|
||||||
|
func (u *User) CanImportLocal() bool {
|
||||||
|
return u.IsAdmin || u.AllowImportLocal
|
||||||
|
}
|
||||||
|
|
||||||
// EmailAdresses is the list of all email addresses of a user. Can contain the
|
// EmailAdresses is the list of all email addresses of a user. Can contain the
|
||||||
// primary email address, but is not obligatory
|
// primary email address, but is not obligatory
|
||||||
type EmailAddress struct {
|
type EmailAddress struct {
|
||||||
|
@ -242,14 +260,12 @@ func (u *User) ValidatePassword(passwd string) bool {
|
||||||
// UploadAvatar saves custom avatar for user.
|
// UploadAvatar saves custom avatar for user.
|
||||||
// FIXME: split uploads to different subdirs in case we have massive users.
|
// FIXME: split uploads to different subdirs in case we have massive users.
|
||||||
func (u *User) UploadAvatar(data []byte) error {
|
func (u *User) UploadAvatar(data []byte) error {
|
||||||
u.UseCustomAvatar = true
|
|
||||||
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(data))
|
img, _, err := image.Decode(bytes.NewReader(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("Decode: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m := resize.Resize(234, 234, img, resize.NearestNeighbor)
|
m := resize.Resize(290, 290, img, resize.NearestNeighbor)
|
||||||
|
|
||||||
sess := x.NewSession()
|
sess := x.NewSession()
|
||||||
defer sessionRelease(sess)
|
defer sessionRelease(sess)
|
||||||
|
@ -257,19 +273,20 @@ func (u *User) UploadAvatar(data []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = sess.Id(u.Id).AllCols().Update(u); err != nil {
|
u.UseCustomAvatar = true
|
||||||
return err
|
if err = updateUser(sess, u); err != nil {
|
||||||
|
return fmt.Errorf("updateUser: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll(setting.AvatarUploadPath, os.ModePerm)
|
os.MkdirAll(setting.AvatarUploadPath, os.ModePerm)
|
||||||
fw, err := os.Create(u.CustomAvatarPath())
|
fw, err := os.Create(u.CustomAvatarPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("Create: %v", err)
|
||||||
}
|
}
|
||||||
defer fw.Close()
|
defer fw.Close()
|
||||||
|
|
||||||
if err = jpeg.Encode(fw, m, nil); err != nil {
|
if err = png.Encode(fw, m); err != nil {
|
||||||
return err
|
return fmt.Errorf("Encode: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
|
@ -356,6 +373,15 @@ func (u *User) DisplayName() string {
|
||||||
return u.Name
|
return u.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShortName returns shorted user name with given maximum length,
|
||||||
|
// it adds "..." at the end if user name has more length than maximum.
|
||||||
|
func (u *User) ShortName(length int) string {
|
||||||
|
if len(u.Name) < length {
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
return u.Name[:length] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
// IsUserExist checks if given user name exist,
|
// IsUserExist checks if given user name exist,
|
||||||
// the user name should be noncased unique.
|
// the user name should be noncased unique.
|
||||||
// If uid is presented, then check will rule out that one,
|
// If uid is presented, then check will rule out that one,
|
||||||
|
@ -717,9 +743,9 @@ func UserPath(userName string) string {
|
||||||
return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
|
return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserByKeyId(keyId int64) (*User, error) {
|
func GetUserByKeyID(keyID int64) (*User, error) {
|
||||||
user := new(User)
|
user := new(User)
|
||||||
has, err := x.Sql("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyId).Get(user)
|
has, err := x.Sql("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyID).Get(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
|
@ -980,7 +1006,7 @@ func GetUserByEmail(email string) (*User, error) {
|
||||||
return GetUserByID(emailAddress.UID)
|
return GetUserByID(emailAddress.UID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrUserNotExist{0, "email"}
|
return nil, ErrUserNotExist{0, email}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchUserByName returns given number of users whose name contains keyword.
|
// SearchUserByName returns given number of users whose name contains keyword.
|
||||||
|
|
|
@ -178,8 +178,8 @@ func GetActiveWebhooksByRepoID(repoID int64) (ws []*Webhook, err error) {
|
||||||
return ws, err
|
return ws, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebhooksByRepoId returns all webhooks of repository.
|
// GetWebhooksByRepoID returns all webhooks of repository.
|
||||||
func GetWebhooksByRepoId(repoID int64) (ws []*Webhook, err error) {
|
func GetWebhooksByRepoID(repoID int64) (ws []*Webhook, err error) {
|
||||||
err = x.Find(&ws, &Webhook{RepoID: repoID})
|
err = x.Find(&ws, &Webhook{RepoID: repoID})
|
||||||
return ws, err
|
return ws, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ type AdminEditUserForm struct {
|
||||||
Active bool
|
Active bool
|
||||||
Admin bool
|
Admin bool
|
||||||
AllowGitHook bool
|
AllowGitHook bool
|
||||||
|
AllowImportLocal bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
|
|
@ -130,7 +130,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
|
||||||
|
|
||||||
l, err := ldapDial(ls)
|
l, err := ldapDial(ls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
|
log.Error(4, "LDAP Connect error (%s): %v", ls.Host, err)
|
||||||
ls.Enabled = false
|
ls.Enabled = false
|
||||||
return "", "", "", false, false
|
return "", "", "", false, false
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
// \/ /_____/ \/ \/ \/ \/ \/
|
// \/ /_____/ \/ \/ \/ \/ \/
|
||||||
|
|
||||||
type CreateOrgForm struct {
|
type CreateOrgForm struct {
|
||||||
OrgName string `binding:"Required;AlphaDashDot;MaxSize(30)" locale:"org.org_name_holder"`
|
OrgName string `binding:"Required;AlphaDashDot;MaxSize(35)" locale:"org.org_name_holder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
@ -25,7 +25,7 @@ func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) bind
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateOrgSettingForm struct {
|
type UpdateOrgSettingForm struct {
|
||||||
Name string `binding:"Required;AlphaDashDot;MaxSize(30)" locale:"org.org_name_holder"`
|
Name string `binding:"Required;AlphaDashDot;MaxSize(35)" locale:"org.org_name_holder"`
|
||||||
FullName string `binding:"MaxSize(100)"`
|
FullName string `binding:"MaxSize(100)"`
|
||||||
Description string `binding:"MaxSize(255)"`
|
Description string `binding:"MaxSize(255)"`
|
||||||
Website string `binding:"Url;MaxSize(100)"`
|
Website string `binding:"Url;MaxSize(100)"`
|
||||||
|
|
|
@ -5,8 +5,14 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
"github.com/go-macaron/binding"
|
"github.com/go-macaron/binding"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
|
|
||||||
|
"github.com/gogits/gogs/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// _______________________________________ _________.______________________ _______________.___.
|
// _______________________________________ _________.______________________ _______________.___.
|
||||||
|
@ -37,8 +43,8 @@ type MigrateRepoForm struct {
|
||||||
AuthPassword string `json:"auth_password"`
|
AuthPassword string `json:"auth_password"`
|
||||||
Uid int64 `json:"uid" binding:"Required"`
|
Uid int64 `json:"uid" binding:"Required"`
|
||||||
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
|
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||||
Private bool `json:"mirror"`
|
Mirror bool `json:"mirror"`
|
||||||
Mirror bool `json:"private"`
|
Private bool `json:"private"`
|
||||||
Description string `json:"description" binding:"MaxSize(255)"`
|
Description string `json:"description" binding:"MaxSize(255)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +52,34 @@ func (f *MigrateRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
|
||||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseRemoteAddr checks if given remote address is valid,
|
||||||
|
// and returns composed URL with needed username and passowrd.
|
||||||
|
// It also checks if given user has permission when remote address
|
||||||
|
// is actually a local path.
|
||||||
|
func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) {
|
||||||
|
remoteAddr := f.CloneAddr
|
||||||
|
|
||||||
|
// Remote address can be HTTP/HTTPS/Git URL or local path.
|
||||||
|
if strings.HasPrefix(remoteAddr, "http://") ||
|
||||||
|
strings.HasPrefix(remoteAddr, "https://") ||
|
||||||
|
strings.HasPrefix(remoteAddr, "git://") {
|
||||||
|
u, err := url.Parse(remoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", models.ErrInvalidCloneAddr{IsURLError: true}
|
||||||
|
}
|
||||||
|
if len(f.AuthUsername)+len(f.AuthPassword) > 0 {
|
||||||
|
u.User = url.UserPassword(f.AuthUsername, f.AuthPassword)
|
||||||
|
}
|
||||||
|
remoteAddr = u.String()
|
||||||
|
} else if !user.CanImportLocal() {
|
||||||
|
return "", models.ErrInvalidCloneAddr{IsPermissionDenied: true}
|
||||||
|
} else if !com.IsDir(remoteAddr) {
|
||||||
|
return "", models.ErrInvalidCloneAddr{IsInvalidPath: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
type RepoSettingForm struct {
|
type RepoSettingForm struct {
|
||||||
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
|
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||||
Description string `binding:"MaxSize(255)"`
|
Description string `binding:"MaxSize(255)"`
|
||||||
|
@ -181,12 +215,12 @@ func (f *CreateLabelForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
|
||||||
// \/ \/ \/ \/ \/ \/
|
// \/ \/ \/ \/ \/ \/
|
||||||
|
|
||||||
type NewReleaseForm struct {
|
type NewReleaseForm struct {
|
||||||
TagName string `form:"tag_name" binding:"Required"`
|
TagName string `binding:"Required"`
|
||||||
Target string `form:"tag_target" binding:"Required"`
|
Target string `form:"tag_target" binding:"Required"`
|
||||||
Title string `form:"title" binding:"Required"`
|
Title string `binding:"Required"`
|
||||||
Content string `form:"content" binding:"Required"`
|
Content string
|
||||||
Draft string `form:"draft"`
|
Draft string
|
||||||
Prerelease bool `form:"prerelease"`
|
Prerelease bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *NewReleaseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
func (f *NewReleaseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
|
|
@ -39,6 +39,8 @@ import (
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//FIXME: remove cache module
|
||||||
|
|
||||||
var gravatarSource string
|
var gravatarSource string
|
||||||
|
|
||||||
func UpdateGravatarSource() {
|
func UpdateGravatarSource() {
|
||||||
|
@ -102,7 +104,7 @@ func New(hash string, cacheDir string) *Avatar {
|
||||||
expireDuration: time.Minute * 10,
|
expireDuration: time.Minute * 10,
|
||||||
reqParams: url.Values{
|
reqParams: url.Values{
|
||||||
"d": {"retro"},
|
"d": {"retro"},
|
||||||
"size": {"200"},
|
"size": {"290"},
|
||||||
"r": {"pg"}}.Encode(),
|
"r": {"pg"}}.Encode(),
|
||||||
imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg
|
imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg
|
||||||
}
|
}
|
||||||
|
@ -153,7 +155,7 @@ func (this *Avatar) Encode(wr io.Writer, size int) (err error) {
|
||||||
if img, err = decodeImageFile(imgPath); err != nil {
|
if img, err = decodeImageFile(imgPath); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m := resize.Resize(uint(size), 0, img, resize.NearestNeighbor)
|
m := resize.Resize(uint(size), 0, img, resize.Lanczos3)
|
||||||
return jpeg.Encode(wr, m, nil)
|
return jpeg.Encode(wr, m, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +194,7 @@ func (this *service) mustInt(r *http.Request, defaultValue int, keys ...string)
|
||||||
func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
urlPath := r.URL.Path
|
urlPath := r.URL.Path
|
||||||
hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
|
hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
|
||||||
size := this.mustInt(r, 80, "s", "size") // default size = 80*80
|
size := this.mustInt(r, 290, "s", "size") // default size = 290*290
|
||||||
|
|
||||||
avatar := New(hash, this.cacheDir)
|
avatar := New(hash, this.cacheDir)
|
||||||
avatar.AlterImage = this.altImage
|
avatar.AlterImage = this.altImage
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
package base
|
package base
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
const DOC_URL = "https://github.com/gogits/go-gogs-client/wiki"
|
const DOC_URL = "https://github.com/gogits/go-gogs-client/wiki"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -11,3 +17,16 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
var GoGetMetas = make(map[string]bool)
|
var GoGetMetas = make(map[string]bool)
|
||||||
|
|
||||||
|
// ExecPath returns the executable path.
|
||||||
|
func ExecPath() (string, error) {
|
||||||
|
file, err := exec.LookPath(os.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
p, err := filepath.Abs(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
"github.com/russross/blackfriday"
|
"github.com/russross/blackfriday"
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
|
|
||||||
|
@ -99,12 +100,33 @@ func (options *CustomRender) Link(out *bytes.Buffer, link []byte, title []byte,
|
||||||
options.Renderer.Link(out, link, title, content)
|
options.Renderer.Link(out, link, title, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
svgSuffix = []byte(".svg")
|
||||||
|
svgSuffixWithMark = []byte(".svg?")
|
||||||
|
)
|
||||||
|
|
||||||
func (options *CustomRender) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
func (options *CustomRender) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||||
if len(link) > 0 && !isLink(link) {
|
prefix := strings.Replace(options.urlPrefix, "/src/", "/raw/", 1)
|
||||||
link = []byte(path.Join(strings.Replace(options.urlPrefix, "/src/", "/raw/", 1), string(link)))
|
if len(link) > 0 {
|
||||||
|
if isLink(link) {
|
||||||
|
// External link with .svg suffix usually means CI status.
|
||||||
|
if bytes.HasSuffix(link, svgSuffix) || bytes.Contains(link, svgSuffixWithMark) {
|
||||||
|
options.Renderer.Image(out, link, title, alt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if link[0] != '/' {
|
||||||
|
prefix += "/"
|
||||||
|
}
|
||||||
|
link = []byte(prefix + string(link))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out.WriteString(`<a href="`)
|
||||||
|
out.Write(link)
|
||||||
|
out.WriteString(`">`)
|
||||||
options.Renderer.Image(out, link, title, alt)
|
options.Renderer.Image(out, link, title, alt)
|
||||||
|
out.WriteString("</a>")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -159,7 +181,21 @@ func RenderSha1CurrentPattern(rawBytes []byte, urlPrefix string) []byte {
|
||||||
return rawBytes
|
return rawBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cutoutVerbosePrefix(prefix string) string {
|
||||||
|
count := 0
|
||||||
|
for i := 0; i < len(prefix); i++ {
|
||||||
|
if prefix[i] == '/' {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if count >= 3 {
|
||||||
|
return prefix[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string) []byte {
|
func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string) []byte {
|
||||||
|
urlPrefix = cutoutVerbosePrefix(urlPrefix)
|
||||||
ms := issueIndexPattern.FindAll(rawBytes, -1)
|
ms := issueIndexPattern.FindAll(rawBytes, -1)
|
||||||
for _, m := range ms {
|
for _, m := range ms {
|
||||||
var space string
|
var space string
|
||||||
|
@ -209,11 +245,21 @@ func RenderRawMarkdown(body []byte, urlPrefix string) []byte {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
leftAngleBracket = []byte("</")
|
||||||
|
rightAngleBracket = []byte(">")
|
||||||
|
)
|
||||||
|
|
||||||
|
var noEndTags = []string{"img", "input", "br", "hr"}
|
||||||
|
|
||||||
// PostProcessMarkdown treats different types of HTML differently,
|
// PostProcessMarkdown treats different types of HTML differently,
|
||||||
// and only renders special links for plain text blocks.
|
// and only renders special links for plain text blocks.
|
||||||
func PostProcessMarkdown(rawHtml []byte, urlPrefix string) []byte {
|
func PostProcessMarkdown(rawHtml []byte, urlPrefix string) []byte {
|
||||||
|
startTags := make([]string, 0, 5)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
tokenizer := html.NewTokenizer(bytes.NewReader(rawHtml))
|
tokenizer := html.NewTokenizer(bytes.NewReader(rawHtml))
|
||||||
|
|
||||||
|
OUTER_LOOP:
|
||||||
for html.ErrorToken != tokenizer.Next() {
|
for html.ErrorToken != tokenizer.Next() {
|
||||||
token := tokenizer.Token()
|
token := tokenizer.Token()
|
||||||
switch token.Type {
|
switch token.Type {
|
||||||
|
@ -225,17 +271,38 @@ func PostProcessMarkdown(rawHtml []byte, urlPrefix string) []byte {
|
||||||
tagName := token.Data
|
tagName := token.Data
|
||||||
// If this is an excluded tag, we skip processing all output until a close tag is encountered.
|
// If this is an excluded tag, we skip processing all output until a close tag is encountered.
|
||||||
if strings.EqualFold("a", tagName) || strings.EqualFold("code", tagName) || strings.EqualFold("pre", tagName) {
|
if strings.EqualFold("a", tagName) || strings.EqualFold("code", tagName) || strings.EqualFold("pre", tagName) {
|
||||||
|
stackNum := 1
|
||||||
for html.ErrorToken != tokenizer.Next() {
|
for html.ErrorToken != tokenizer.Next() {
|
||||||
token = tokenizer.Token()
|
token = tokenizer.Token()
|
||||||
|
|
||||||
// Copy the token to the output verbatim
|
// Copy the token to the output verbatim
|
||||||
buf.WriteString(token.String())
|
buf.WriteString(token.String())
|
||||||
// If this is the close tag, we are done
|
|
||||||
if html.EndTagToken == token.Type && strings.EqualFold(tagName, token.Data) {
|
if token.Type == html.StartTagToken {
|
||||||
|
stackNum++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the close tag to the outer-most, we are done
|
||||||
|
if token.Type == html.EndTagToken && strings.EqualFold(tagName, token.Data) {
|
||||||
|
stackNum--
|
||||||
|
|
||||||
|
if stackNum == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
continue OUTER_LOOP
|
||||||
|
}
|
||||||
|
|
||||||
|
if !com.IsSliceContainsStr(noEndTags, token.Data) {
|
||||||
|
startTags = append(startTags, token.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
case html.EndTagToken:
|
||||||
|
buf.Write(leftAngleBracket)
|
||||||
|
buf.WriteString(startTags[len(startTags)-1])
|
||||||
|
buf.Write(rightAngleBracket)
|
||||||
|
startTags = startTags[:len(startTags)-1]
|
||||||
default:
|
default:
|
||||||
buf.WriteString(token.String())
|
buf.WriteString(token.String())
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"github.com/Unknwon/i18n"
|
"github.com/Unknwon/i18n"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
|
||||||
|
"github.com/gogits/chardet"
|
||||||
|
|
||||||
"github.com/gogits/gogs/modules/avatar"
|
"github.com/gogits/gogs/modules/avatar"
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
)
|
)
|
||||||
|
@ -43,6 +45,22 @@ func EncodeSha1(str string) string {
|
||||||
return hex.EncodeToString(h.Sum(nil))
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ShortSha(sha1 string) string {
|
||||||
|
if len(sha1) == 40 {
|
||||||
|
return sha1[:10]
|
||||||
|
}
|
||||||
|
return sha1
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetectEncoding(content []byte) (string, error) {
|
||||||
|
detector := chardet.NewTextDetector()
|
||||||
|
result, err := detector.DetectBest(content)
|
||||||
|
if result.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
|
||||||
|
return setting.Repository.AnsiCharset, err
|
||||||
|
}
|
||||||
|
return result.Charset, err
|
||||||
|
}
|
||||||
|
|
||||||
func BasicAuthDecode(encoded string) (string, string, error) {
|
func BasicAuthDecode(encoded string) (string, string, error) {
|
||||||
s, err := base64.StdEncoding.DecodeString(encoded)
|
s, err := base64.StdEncoding.DecodeString(encoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -111,7 +111,7 @@ func TestSpecSchedule(t *testing.T) {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(actual, c.expected) {
|
if !reflect.DeepEqual(actual, c.expected) {
|
||||||
t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
|
t.Errorf("%s => (expected) %v != %v (actual)", c.expr, c.expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,615 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,287 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
// 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()
|
|
||||||
}
|
|
|
@ -1,184 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,209 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
// 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()
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,98 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,501 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,216 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,631 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,549 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,213 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,441 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,393 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,354 +0,0 @@
|
||||||
// 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()
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
// 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
|
|
|
@ -1,211 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,412 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,415 +0,0 @@
|
||||||
// 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()
|
|
||||||
}
|
|
|
@ -1,526 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,628 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,306 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
// 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)}
|
|
||||||
}},
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
// 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.")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,725 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,254 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,356 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,525 +0,0 @@
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,493 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,605 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue