mirror of
https://github.com/go-i2p/reseed-tools.git
synced 2025-09-23 22:33:23 -04:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
83cf8bdcde | ||
![]() |
2a23c5ea13 | ||
![]() |
a666afef7c |
60
.github/workflows/page.yaml
vendored
60
.github/workflows/page.yaml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Generate and Deploy GitHub Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Run once hourly
|
|
||||||
schedule:
|
|
||||||
- cron: '0 * * * *'
|
|
||||||
# Allow manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
# Run on pushes to main branch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Fetch all history for proper repo data
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.24.x'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Build Site Generator
|
|
||||||
run: |
|
|
||||||
go install github.com/go-i2p/go-gh-page/cmd/github-site-gen@latest
|
|
||||||
export GOBIN=$(go env GOPATH)/bin
|
|
||||||
cp -v "$GOBIN/github-site-gen" ./github-site-gen
|
|
||||||
# Ensure the binary is executable
|
|
||||||
chmod +x github-site-gen
|
|
||||||
|
|
||||||
- name: Generate Site
|
|
||||||
run: |
|
|
||||||
# Determine current repository owner and name
|
|
||||||
REPO_OWNER=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 1)
|
|
||||||
REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 2)
|
|
||||||
|
|
||||||
# Generate the site
|
|
||||||
./github-site-gen -repo "${REPO_OWNER}/${REPO_NAME}" -output ./site
|
|
||||||
|
|
||||||
# Create a .nojekyll file to disable Jekyll processing
|
|
||||||
touch ./site/.nojekyll
|
|
||||||
|
|
||||||
# Add a .gitattributes file to ensure consistent line endings
|
|
||||||
echo "* text=auto" > ./site/.gitattributes
|
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
uses: JamesIves/github-pages-deploy-action@v4
|
|
||||||
with:
|
|
||||||
folder: site # The folder the action should deploy
|
|
||||||
branch: gh-pages # The branch the action should deploy to
|
|
||||||
clean: true # Automatically remove deleted files from the deploy branch
|
|
||||||
commit-message: "Deploy site generated on ${{ github.sha }}"
|
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -2,9 +2,9 @@
|
|||||||
/cert.pem
|
/cert.pem
|
||||||
/key.pem
|
/key.pem
|
||||||
/_netdb
|
/_netdb
|
||||||
i2pkeys
|
/i2pkeys
|
||||||
onionkeys
|
/onionkeys
|
||||||
tlskeys
|
/tlskeys
|
||||||
/tmp
|
/tmp
|
||||||
i2pseeds.su3
|
i2pseeds.su3
|
||||||
*.pem
|
*.pem
|
||||||
@@ -21,4 +21,6 @@ audit.json
|
|||||||
*ed25519*
|
*ed25519*
|
||||||
client.yaml
|
client.yaml
|
||||||
plugin.yaml
|
plugin.yaml
|
||||||
err
|
err
|
||||||
|
/plugin-linux-amd64.yaml
|
||||||
|
/client-linux-amd64.yaml
|
215
CHANGELOG.html
Normal file
215
CHANGELOG.html
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
I2P Reseed Tools
|
||||||
|
</title>
|
||||||
|
<meta name="author" content="eyedeekay" />
|
||||||
|
<meta name="description" content="reseed-tools" />
|
||||||
|
<meta name="keywords" content="master" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="showhider.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="navbar">
|
||||||
|
<a href="#shownav">
|
||||||
|
Show navigation
|
||||||
|
</a>
|
||||||
|
<div id="shownav">
|
||||||
|
<div id="hidenav">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="..">
|
||||||
|
Up one level ^
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="index.html">
|
||||||
|
index
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="CHANGELOG.html">
|
||||||
|
CHANGELOG
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="content/index.html">
|
||||||
|
content/index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/index.html">
|
||||||
|
docs/index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="index.html">
|
||||||
|
index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/DEBIAN.html">
|
||||||
|
docs/DEBIAN
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/DOCKER.html">
|
||||||
|
docs/DOCKER
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/EXAMPLES.html">
|
||||||
|
docs/EXAMPLES
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/PLUGIN.html">
|
||||||
|
docs/PLUGIN
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/index.html">
|
||||||
|
docs/index
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/SERVICES.html">
|
||||||
|
docs/SERVICES
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/TLS.html">
|
||||||
|
docs/TLS
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/index.html">
|
||||||
|
docs/index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
<a href="#hidenav">
|
||||||
|
Hide Navigation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a id="returnhome" href="/">
|
||||||
|
/
|
||||||
|
</a>
|
||||||
|
<p>
|
||||||
|
2021-12-16
|
||||||
|
* app.Version = “0.2.11”
|
||||||
|
* include license file in plugin
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2021-12-14
|
||||||
|
* app.Version = “0.2.10”
|
||||||
|
* restart changelog
|
||||||
|
* fix websiteURL in plugin.config
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2019-04-21
|
||||||
|
* app.Version = “0.1.7”
|
||||||
|
* enabling TLS 1.3
|
||||||
|
<em>
|
||||||
|
only
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2016-12-21
|
||||||
|
* deactivating previous random time delta, makes only sense when patching ri too
|
||||||
|
* app.Version = “0.1.6”
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2016-10-09
|
||||||
|
* seed the math random generator with time.Now().UnixNano()
|
||||||
|
* added 6h+6h random time delta at su3-age to increase anonymity
|
||||||
|
* app.Version = “0.1.5”
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2016-05-15
|
||||||
|
* README.md updated
|
||||||
|
* allowed routerInfos age increased from 96 to 192 hours
|
||||||
|
* app.Version = “0.1.4”
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2016-03-05
|
||||||
|
* app.Version = “0.1.3”
|
||||||
|
* CRL creation added
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2016-01-31
|
||||||
|
* allowed TLS ciphers updated (hardened)
|
||||||
|
* TLS certificate generation: RSA 4096 –> ECDSAWithSHA512 384bit secp384r1
|
||||||
|
* ECDHE handshake: only CurveP384 + CurveP521, default CurveP256 removed
|
||||||
|
* TLS certificate valid: 2y –> 5y
|
||||||
|
* throttled.PerDay(4) –> PerHour(4), to enable limited testing
|
||||||
|
* su3 RebuildInterval: 24h –> 90h, higher anonymity for the running i2p-router
|
||||||
|
* numRi per su3 file: 75 –> 77
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2016-01
|
||||||
|
* fork from
|
||||||
|
<a href="https://i2pgit.org/idk/reseed-tools">
|
||||||
|
https://i2pgit.org/idk/reseed-tools
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div id="sourcecode">
|
||||||
|
<span id="sourcehead">
|
||||||
|
<strong>
|
||||||
|
Get the source code:
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://i2pgit.org/idk/reseed-tools">
|
||||||
|
Source Repository: (https://i2pgit.org/idk/reseed-tools)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="#show">
|
||||||
|
Show license
|
||||||
|
</a>
|
||||||
|
<div id="show">
|
||||||
|
<div id="hide">
|
||||||
|
<pre><code>Copyright (c) 2014 Matt Drollette
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
</code></pre>
|
||||||
|
<a href="#hide">
|
||||||
|
Hide license
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<iframe src="https://snowflake.torproject.org/embed.html" width="320" height="240" frameborder="0" scrolling="no"></iframe>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://geti2p.net/">
|
||||||
|
<img src="i2plogo.png"></img>
|
||||||
|
I2P
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
Makefile
16
Makefile
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
VERSION=$(shell /usr/bin/go run . version 2>/dev/null)
|
VERSION=$(shell /usr/bin/go run . version 2>/dev/null)
|
||||||
APP=reseed-tools
|
APP=reseed-tools
|
||||||
USER_GH=go-i2p
|
USER_GH=eyedeekay
|
||||||
SIGNER=hankhill19580@gmail.com
|
SIGNER=hankhill19580@gmail.com
|
||||||
CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
@@ -28,9 +28,6 @@ echo:
|
|||||||
host:
|
host:
|
||||||
/usr/bin/go build -o reseed-tools-host 2>/dev/null 1>/dev/null
|
/usr/bin/go build -o reseed-tools-host 2>/dev/null 1>/dev/null
|
||||||
|
|
||||||
testrun:
|
|
||||||
DEBUG_I2P=debug go run . reseed --yes --signer=example@mail.i2p
|
|
||||||
|
|
||||||
index:
|
index:
|
||||||
edgar
|
edgar
|
||||||
|
|
||||||
@@ -119,8 +116,13 @@ jar: gojava
|
|||||||
|
|
||||||
release: version plugins upload-su3s
|
release: version plugins upload-su3s
|
||||||
|
|
||||||
|
tag:
|
||||||
|
git tag -a v$(VERSION) -m "Release $(VERSION)"
|
||||||
|
git push --tags
|
||||||
|
|
||||||
version:
|
version:
|
||||||
head -n 5 README.md | github-release release -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(APP) -t v$(VERSION) -d -; true
|
#head -n 5 README.md | github-release release -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(APP) -t v$(VERSION) -d -; true
|
||||||
|
echo "make version is deprecated, use make tag instead"
|
||||||
|
|
||||||
delete-version:
|
delete-version:
|
||||||
github-release delete -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(APP) -t v$(VERSION)
|
github-release delete -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(APP) -t v$(VERSION)
|
||||||
@@ -177,7 +179,7 @@ upload-su3s:
|
|||||||
export GOOS=windows; export GOARCH=386; make upload-single-su3
|
export GOOS=windows; export GOARCH=386; make upload-single-su3
|
||||||
|
|
||||||
download-single-su3:
|
download-single-su3:
|
||||||
wget-ds "https://github.com/go-i2p/reseed-tools/releases/download/v$(VERSION)/reseed-tools-$(GOOS)-$(GOARCH).su3"
|
wget-ds "https://github.com/eyedeekay/reseed-tools/releases/download/v$(VERSION)/reseed-tools-$(GOOS)-$(GOARCH).su3"
|
||||||
|
|
||||||
upload-single-su3:
|
upload-single-su3:
|
||||||
github-release upload -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(APP) -t v$(VERSION) -f reseed-tools-"$(GOOS)"-"$(GOARCH).su3" -l "`sha256sum reseed-tools-$(GOOS)-$(GOARCH).su3`" -n "reseed-tools-$(GOOS)"-"$(GOARCH).su3"; true
|
github-release upload -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(APP) -t v$(VERSION) -f reseed-tools-"$(GOOS)"-"$(GOARCH).su3" -l "`sha256sum reseed-tools-$(GOOS)-$(GOARCH).su3`" -n "reseed-tools-$(GOOS)"-"$(GOARCH).su3"; true
|
||||||
@@ -194,7 +196,7 @@ tmp/lib:
|
|||||||
tmp/LICENSE:
|
tmp/LICENSE:
|
||||||
cp LICENSE tmp/LICENSE
|
cp LICENSE tmp/LICENSE
|
||||||
|
|
||||||
SIGNER_DIR=$(HOME)/i2p-go-keys.bak/
|
SIGNER_DIR=$(HOME)/i2p-go-keys/
|
||||||
|
|
||||||
su3s: tmp/content tmp/lib tmp/LICENSE build
|
su3s: tmp/content tmp/lib tmp/LICENSE build
|
||||||
rm -f plugin.yaml client.yaml
|
rm -f plugin.yaml client.yaml
|
||||||
|
39
README.md
39
README.md
@@ -13,8 +13,8 @@ included, apply on [i2pforum.i2p](http://i2pforum.i2p).
|
|||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
`go`, `git`, and optionally `make` are required to build the project.
|
`go`, `git`, and optionally `make` are required to build the project.
|
||||||
Precompiled binaries for most platforms are available at the github mirror
|
Precompiled binaries for most platforms are available at my github mirror
|
||||||
https://github.com/go-i2p/reseed-tools.
|
https://github.com/eyedeekay/i2p-tools-1.
|
||||||
|
|
||||||
In order to install the build-dependencies on Ubuntu or Debian, you may use:
|
In order to install the build-dependencies on Ubuntu or Debian, you may use:
|
||||||
|
|
||||||
@@ -39,40 +39,6 @@ make build
|
|||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Logging Configuration
|
|
||||||
|
|
||||||
The reseed-tools uses structured logging with configurable verbosity levels via the `github.com/go-i2p/logger` package. Logging is controlled through environment variables:
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
- **`DEBUG_I2P`**: Controls logging verbosity levels
|
|
||||||
- `debug` - Enable debug level logging (most verbose)
|
|
||||||
- `warn` - Enable warning level logging
|
|
||||||
- `error` - Enable error level logging only
|
|
||||||
- Not set - Logging disabled (default)
|
|
||||||
|
|
||||||
- **`WARNFAIL_I2P`**: Enable fast-fail mode for testing
|
|
||||||
- `true` - Warnings and errors become fatal for robust testing
|
|
||||||
- Not set - Normal operation (default)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable debug logging
|
|
||||||
export DEBUG_I2P=debug
|
|
||||||
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb
|
|
||||||
|
|
||||||
# Enable warning/error logging with fast-fail for testing
|
|
||||||
export DEBUG_I2P=warn
|
|
||||||
export WARNFAIL_I2P=true
|
|
||||||
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb
|
|
||||||
|
|
||||||
# Production mode (no logging)
|
|
||||||
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb
|
|
||||||
```
|
|
||||||
|
|
||||||
The structured logging provides rich context for debugging I2P network operations, server startup, and file processing while maintaining zero performance impact in production when logging is disabled.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
#### Debian/Ubuntu note:
|
#### Debian/Ubuntu note:
|
||||||
@@ -107,3 +73,4 @@ reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --port=84
|
|||||||
|
|
||||||
- **Usage** [More examples can be found here.](docs/EXAMPLES.md)
|
- **Usage** [More examples can be found here.](docs/EXAMPLES.md)
|
||||||
- **Docker** [Docker examples can be found here](docs/DOCKER.md)
|
- **Docker** [Docker examples can be found here](docs/DOCKER.md)
|
||||||
|
|
316
cmd/diagnose.go
316
cmd/diagnose.go
@@ -1,316 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-i2p/common/router_info"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewDiagnoseCommand creates a new CLI command for diagnosing RouterInfo files
|
|
||||||
// in the netDb directory to identify corrupted or problematic files that cause
|
|
||||||
// parsing errors during reseed operations.
|
|
||||||
func NewDiagnoseCommand() *cli.Command {
|
|
||||||
return &cli.Command{
|
|
||||||
Name: "diagnose",
|
|
||||||
Usage: "Diagnose RouterInfo files in netDb to identify parsing issues",
|
|
||||||
Description: `Scan RouterInfo files in the netDb directory to identify files that cause
|
|
||||||
parsing errors. This can help identify corrupted files that should be removed
|
|
||||||
to prevent "mapping format violation" errors during reseed operations.`,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "netdb",
|
|
||||||
Aliases: []string{"n"},
|
|
||||||
Usage: "Path to the netDb directory containing RouterInfo files",
|
|
||||||
Value: findDefaultNetDbPath(),
|
|
||||||
Required: false,
|
|
||||||
},
|
|
||||||
&cli.DurationFlag{
|
|
||||||
Name: "max-age",
|
|
||||||
Aliases: []string{"a"},
|
|
||||||
Usage: "Maximum age for RouterInfo files to consider (e.g., 192h for 8 days)",
|
|
||||||
Value: 192 * time.Hour, // Default matches reseed server
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "remove-bad",
|
|
||||||
Aliases: []string{"r"},
|
|
||||||
Usage: "Remove files that fail parsing (use with caution)",
|
|
||||||
Value: false,
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "verbose",
|
|
||||||
Aliases: []string{"v"},
|
|
||||||
Usage: "Enable verbose output",
|
|
||||||
Value: false,
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "debug",
|
|
||||||
Aliases: []string{"d"},
|
|
||||||
Usage: "Enable debug mode (sets I2P_DEBUG=true)",
|
|
||||||
Value: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Action: diagnoseRouterInfoFiles,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// diagnoseRouterInfoFiles performs the main diagnosis logic for RouterInfo files
|
|
||||||
func diagnoseRouterInfoFiles(ctx *cli.Context) error {
|
|
||||||
config, err := extractDiagnosisConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateNetDbPath(config.netdbPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
printDiagnosisHeader(config)
|
|
||||||
|
|
||||||
routerInfoPattern, err := compileRouterInfoPattern()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := &diagnosisStats{}
|
|
||||||
|
|
||||||
err = filepath.WalkDir(config.netdbPath, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
return processRouterInfoFile(path, d, err, routerInfoPattern, config, stats)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error walking netDb directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
printDiagnosisSummary(stats, config.removeBad)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// diagnosisConfig holds all configuration parameters for diagnosis
|
|
||||||
type diagnosisConfig struct {
|
|
||||||
netdbPath string
|
|
||||||
maxAge time.Duration
|
|
||||||
removeBad bool
|
|
||||||
verbose bool
|
|
||||||
debug bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// diagnosisStats tracks file processing statistics
|
|
||||||
type diagnosisStats struct {
|
|
||||||
totalFiles int
|
|
||||||
tooOldFiles int
|
|
||||||
corruptedFiles int
|
|
||||||
validFiles int
|
|
||||||
removedFiles int
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractDiagnosisConfig extracts and validates configuration from CLI context
|
|
||||||
func extractDiagnosisConfig(ctx *cli.Context) (*diagnosisConfig, error) {
|
|
||||||
config := &diagnosisConfig{
|
|
||||||
netdbPath: ctx.String("netdb"),
|
|
||||||
maxAge: ctx.Duration("max-age"),
|
|
||||||
removeBad: ctx.Bool("remove-bad"),
|
|
||||||
verbose: ctx.Bool("verbose"),
|
|
||||||
debug: ctx.Bool("debug"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set debug mode if requested
|
|
||||||
if config.debug {
|
|
||||||
os.Setenv("I2P_DEBUG", "true")
|
|
||||||
fmt.Println("Debug mode enabled (I2P_DEBUG=true)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.netdbPath == "" {
|
|
||||||
return nil, fmt.Errorf("netDb path is required. Use --netdb flag or ensure I2P is installed in a standard location")
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateNetDbPath checks if the netDb directory exists
|
|
||||||
func validateNetDbPath(netdbPath string) error {
|
|
||||||
if _, err := os.Stat(netdbPath); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("netDb directory does not exist: %s", netdbPath)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// printDiagnosisHeader prints the diagnosis configuration information
|
|
||||||
func printDiagnosisHeader(config *diagnosisConfig) {
|
|
||||||
fmt.Printf("Diagnosing RouterInfo files in: %s\n", config.netdbPath)
|
|
||||||
fmt.Printf("Maximum file age: %v\n", config.maxAge)
|
|
||||||
fmt.Printf("Remove bad files: %v\n", config.removeBad)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// compileRouterInfoPattern compiles the regex pattern for RouterInfo files
|
|
||||||
func compileRouterInfoPattern() (*regexp.Regexp, error) {
|
|
||||||
pattern, err := regexp.Compile(`^routerInfo-[A-Za-z0-9-=~]+\.dat$`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to compile regex pattern: %v", err)
|
|
||||||
}
|
|
||||||
return pattern, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processRouterInfoFile handles individual RouterInfo file processing
|
|
||||||
func processRouterInfoFile(path string, d fs.DirEntry, err error, pattern *regexp.Regexp, config *diagnosisConfig, stats *diagnosisStats) error {
|
|
||||||
if err != nil {
|
|
||||||
if config.verbose {
|
|
||||||
fmt.Printf("Error accessing path %s: %v\n", path, err)
|
|
||||||
}
|
|
||||||
return nil // Continue processing other files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip directories
|
|
||||||
if d.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file matches RouterInfo pattern
|
|
||||||
if !pattern.MatchString(d.Name()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.totalFiles++
|
|
||||||
|
|
||||||
// Get file info and check age
|
|
||||||
if shouldSkipOldFile(path, d, config, stats) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to read and parse the RouterInfo file
|
|
||||||
return analyzeRouterInfoFile(path, config, stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldSkipOldFile checks if file should be skipped due to age
|
|
||||||
func shouldSkipOldFile(path string, d fs.DirEntry, config *diagnosisConfig, stats *diagnosisStats) bool {
|
|
||||||
info, err := d.Info()
|
|
||||||
if err != nil {
|
|
||||||
if config.verbose {
|
|
||||||
fmt.Printf("Error getting file info for %s: %v\n", path, err)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
age := time.Since(info.ModTime())
|
|
||||||
if age > config.maxAge {
|
|
||||||
stats.tooOldFiles++
|
|
||||||
if config.verbose {
|
|
||||||
fmt.Printf("SKIP (too old): %s (age: %v)\n", path, age)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// analyzeRouterInfoFile reads and analyzes a RouterInfo file
|
|
||||||
func analyzeRouterInfoFile(path string, config *diagnosisConfig, stats *diagnosisStats) error {
|
|
||||||
routerBytes, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("ERROR reading %s: %v\n", path, err)
|
|
||||||
stats.corruptedFiles++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse RouterInfo using the same approach as the reseed server
|
|
||||||
riStruct, remainder, err := router_info.ReadRouterInfo(routerBytes)
|
|
||||||
if err != nil {
|
|
||||||
return handleCorruptedFile(path, err, remainder, config, stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
return validateRouterInfo(path, riStruct, config, stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCorruptedFile processes files that fail parsing
|
|
||||||
func handleCorruptedFile(path string, parseErr error, remainder []byte, config *diagnosisConfig, stats *diagnosisStats) error {
|
|
||||||
fmt.Printf("CORRUPTED: %s - %v\n", path, parseErr)
|
|
||||||
if len(remainder) > 0 {
|
|
||||||
fmt.Printf(" Leftover data: %d bytes\n", len(remainder))
|
|
||||||
if config.verbose {
|
|
||||||
maxBytes := len(remainder)
|
|
||||||
if maxBytes > 50 {
|
|
||||||
maxBytes = 50
|
|
||||||
}
|
|
||||||
fmt.Printf(" First %d bytes of remainder: %x\n", maxBytes, remainder[:maxBytes])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stats.corruptedFiles++
|
|
||||||
|
|
||||||
// Remove file if requested
|
|
||||||
if config.removeBad {
|
|
||||||
if removeErr := os.Remove(path); removeErr != nil {
|
|
||||||
fmt.Printf(" ERROR removing file: %v\n", removeErr)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" REMOVED\n")
|
|
||||||
stats.removedFiles++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateRouterInfo performs additional checks on valid RouterInfo structures
|
|
||||||
func validateRouterInfo(path string, riStruct router_info.RouterInfo, config *diagnosisConfig, stats *diagnosisStats) error {
|
|
||||||
gv, err := riStruct.GoodVersion()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Version check error %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.validFiles++
|
|
||||||
if config.verbose {
|
|
||||||
if riStruct.Reachable() && riStruct.UnCongested() && gv {
|
|
||||||
fmt.Printf("OK: %s (reachable, uncongested, good version)\n", path)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("OK: %s (but would be skipped by reseed: reachable=%v uncongested=%v goodversion=%v)\n",
|
|
||||||
path, riStruct.Reachable(), riStruct.UnCongested(), gv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// printDiagnosisSummary prints the final diagnosis results
|
|
||||||
func printDiagnosisSummary(stats *diagnosisStats, removeBad bool) {
|
|
||||||
fmt.Println("\n=== DIAGNOSIS SUMMARY ===")
|
|
||||||
fmt.Printf("Total RouterInfo files found: %d\n", stats.totalFiles)
|
|
||||||
fmt.Printf("Files too old (skipped): %d\n", stats.tooOldFiles)
|
|
||||||
fmt.Printf("Valid files: %d\n", stats.validFiles)
|
|
||||||
fmt.Printf("Corrupted files: %d\n", stats.corruptedFiles)
|
|
||||||
if removeBad {
|
|
||||||
fmt.Printf("Files removed: %d\n", stats.removedFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stats.corruptedFiles > 0 {
|
|
||||||
fmt.Printf("\nFound %d corrupted RouterInfo files causing parsing errors.\n", stats.corruptedFiles)
|
|
||||||
if !removeBad {
|
|
||||||
fmt.Println("To remove them, run this command again with --remove-bad flag.")
|
|
||||||
}
|
|
||||||
fmt.Println("These files are likely causing the 'mapping format violation' errors you're seeing.")
|
|
||||||
} else {
|
|
||||||
fmt.Println("\nNo corrupted RouterInfo files found. The parsing errors may be transient.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// findDefaultNetDbPath attempts to find the default netDb path for the current system
|
|
||||||
func findDefaultNetDbPath() string {
|
|
||||||
// Common I2P netDb locations
|
|
||||||
possiblePaths := []string{
|
|
||||||
os.ExpandEnv("$HOME/.i2p/netDb"),
|
|
||||||
os.ExpandEnv("$HOME/Library/Application Support/i2p/netDb"),
|
|
||||||
"/var/lib/i2p/i2p-config/netDb",
|
|
||||||
"/usr/share/i2p/netDb",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range possiblePaths {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "" // Return empty if not found
|
|
||||||
}
|
|
@@ -7,10 +7,6 @@ import (
|
|||||||
i2pd "github.com/eyedeekay/go-i2pd/goi2pd"
|
i2pd "github.com/eyedeekay/go-i2pd/goi2pd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitializeI2PD initializes an I2PD SAM interface for I2P network connectivity.
|
|
||||||
// It returns a cleanup function that should be called when the I2P connection is no longer needed.
|
|
||||||
// This function is only available when building with the i2pd build tag.
|
|
||||||
func InitializeI2PD() func() {
|
func InitializeI2PD() func() {
|
||||||
// Initialize I2P SAM interface with default configuration
|
|
||||||
return i2pd.InitI2PSAM(nil)
|
return i2pd.InitI2PSAM(nil)
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,3 @@
|
|||||||
// Package cmd provides command-line interface implementations for reseed-tools.
|
|
||||||
// This package contains all CLI commands for key generation, server operation, file verification,
|
|
||||||
// and network database sharing operations. Each command is self-contained and provides
|
|
||||||
// comprehensive functionality for I2P network reseed operations.
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,9 +6,7 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewKeygenCommand creates a new CLI command for generating cryptographic keys.
|
// NewKeygenCommand creates a new CLI command for generating keys.
|
||||||
// It supports generating signing keys for SU3 file signing and TLS certificates for HTTPS serving.
|
|
||||||
// Users can specify either --signer for SU3 signing keys or --tlsHost for TLS certificates.
|
|
||||||
func NewKeygenCommand() *cli.Command {
|
func NewKeygenCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "keygen",
|
Name: "keygen",
|
||||||
@@ -36,27 +30,21 @@ func keygenAction(c *cli.Context) error {
|
|||||||
tlsHost := c.String("tlsHost")
|
tlsHost := c.String("tlsHost")
|
||||||
trustProxy := c.Bool("trustProxy")
|
trustProxy := c.Bool("trustProxy")
|
||||||
|
|
||||||
// Validate that at least one key generation option is specified
|
|
||||||
if signerID == "" && tlsHost == "" {
|
if signerID == "" && tlsHost == "" {
|
||||||
fmt.Println("You must specify either --tlsHost or --signer")
|
fmt.Println("You must specify either --tlsHost or --signer")
|
||||||
lgr.Error("Key generation requires either --tlsHost or --signer parameter")
|
|
||||||
return fmt.Errorf("You must specify either --tlsHost or --signer")
|
return fmt.Errorf("You must specify either --tlsHost or --signer")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate signing certificate if signer ID is provided
|
|
||||||
if signerID != "" {
|
if signerID != "" {
|
||||||
if err := createSigningCertificate(signerID); nil != err {
|
if err := createSigningCertificate(signerID); nil != err {
|
||||||
lgr.WithError(err).WithField("signer_id", signerID).Error("Failed to create signing certificate")
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate TLS certificate if host is provided and proxy trust is enabled
|
|
||||||
if trustProxy {
|
if trustProxy {
|
||||||
if tlsHost != "" {
|
if tlsHost != "" {
|
||||||
if err := createTLSCertificate(tlsHost); nil != err {
|
if err := createTLSCertificate(tlsHost); nil != err {
|
||||||
lgr.WithError(err).WithField("tls_host", tlsHost).Error("Failed to create TLS certificate")
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -1,51 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/registration"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MyUser represents an ACME user for Let's Encrypt certificate generation.
|
|
||||||
// It implements the required interface for ACME protocol interactions including
|
|
||||||
// email registration, private key management, and certificate provisioning.
|
|
||||||
// Taken directly from the lego example, since we need very minimal support
|
|
||||||
// https://go-acme.github.io/lego/usage/library/
|
|
||||||
// Moved from: utils.go
|
|
||||||
type MyUser struct {
|
|
||||||
Email string
|
|
||||||
Registration *registration.Resource
|
|
||||||
key crypto.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMyUser creates a new ACME user with the given email and private key.
|
|
||||||
// The email is used for ACME registration and the private key for cryptographic operations.
|
|
||||||
// Returns a configured MyUser instance ready for certificate generation.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func NewMyUser(email string, key crypto.PrivateKey) *MyUser {
|
|
||||||
return &MyUser{
|
|
||||||
Email: email,
|
|
||||||
key: key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEmail returns the user's email address for ACME registration.
|
|
||||||
// This method is required by the ACME user interface for account identification.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func (u *MyUser) GetEmail() string {
|
|
||||||
return u.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRegistration returns the user's ACME registration resource.
|
|
||||||
// Contains registration details and account information from the ACME server.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func (u MyUser) GetRegistration() *registration.Resource {
|
|
||||||
return u.Registration
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrivateKey returns the user's private key for ACME operations.
|
|
||||||
// Used for signing ACME requests and certificate generation processes.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
|
|
||||||
return u.key
|
|
||||||
}
|
|
766
cmd/reseed.go
766
cmd/reseed.go
File diff suppressed because it is too large
Load Diff
46
cmd/share.go
46
cmd/share.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -14,18 +15,16 @@ import (
|
|||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
|
|
||||||
"github.com/go-i2p/checki2cp/getmeanetdb"
|
"github.com/eyedeekay/checki2cp/getmeanetdb"
|
||||||
"github.com/go-i2p/onramp"
|
"github.com/eyedeekay/onramp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewShareCommand creates a new CLI command for sharing the netDb over I2P with password protection.
|
// NewShareCommand creates a new CLI Command for sharing the netDb over I2P with a password.
|
||||||
// This command sets up a secure file sharing server that allows remote I2P routers to access
|
|
||||||
// and download router information from the local netDb directory for network synchronization.
|
|
||||||
// Can be used to combine the local netDb with the netDb of a remote I2P router.
|
// Can be used to combine the local netDb with the netDb of a remote I2P router.
|
||||||
func NewShareCommand() *cli.Command {
|
func NewShareCommand() *cli.Command {
|
||||||
ndb, err := getmeanetdb.WhereIstheNetDB()
|
ndb, err := getmeanetdb.WhereIstheNetDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).Fatal("Fatal error in share")
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "share",
|
Name: "share",
|
||||||
@@ -60,9 +59,6 @@ func NewShareCommand() *cli.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharer implements a password-protected HTTP file server for netDb sharing.
|
|
||||||
// It wraps the standard HTTP file system with authentication middleware to ensure
|
|
||||||
// only authorized clients can access router information over the I2P network.
|
|
||||||
type sharer struct {
|
type sharer struct {
|
||||||
http.FileSystem
|
http.FileSystem
|
||||||
http.Handler
|
http.Handler
|
||||||
@@ -71,7 +67,6 @@ type sharer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *sharer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *sharer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// Extract password from custom reseed-password header
|
|
||||||
p, ok := r.Header[http.CanonicalHeaderKey("reseed-password")]
|
p, ok := r.Header[http.CanonicalHeaderKey("reseed-password")]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -79,9 +74,9 @@ func (s *sharer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
if p[0] != s.Password {
|
if p[0] != s.Password {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lgr.WithField("path", r.URL.Path).Debug("Request path")
|
log.Println("Path", r.URL.Path)
|
||||||
if strings.HasSuffix(r.URL.Path, "tar.gz") {
|
if strings.HasSuffix(r.URL.Path, "tar.gz") {
|
||||||
lgr.Debug("Serving netdb")
|
log.Println("Serving netdb")
|
||||||
archive, err := walker(s.Path)
|
archive, err := walker(s.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -92,83 +87,62 @@ func (s *sharer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.Handler.ServeHTTP(w, r)
|
s.Handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sharer creates a new HTTP file server for sharing netDb files over I2P.
|
|
||||||
// It sets up a password-protected file system server that can serve router information
|
|
||||||
// to other I2P nodes. The netDbDir parameter specifies the directory containing router files.
|
|
||||||
func Sharer(netDbDir, password string) *sharer {
|
func Sharer(netDbDir, password string) *sharer {
|
||||||
fileSystem := &sharer{
|
fileSystem := &sharer{
|
||||||
FileSystem: http.Dir(netDbDir),
|
FileSystem: http.Dir(netDbDir),
|
||||||
Path: netDbDir,
|
Path: netDbDir,
|
||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
// Configure HTTP file server for the netDb directory
|
|
||||||
fileSystem.Handler = http.FileServer(fileSystem.FileSystem)
|
fileSystem.Handler = http.FileServer(fileSystem.FileSystem)
|
||||||
return fileSystem
|
return fileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareAction(c *cli.Context) error {
|
func shareAction(c *cli.Context) error {
|
||||||
// Convert netDb path to absolute path for consistent file access
|
|
||||||
netDbDir, err := filepath.Abs(c.String("netdb"))
|
netDbDir, err := filepath.Abs(c.String("netdb"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Create password-protected file server for netDb sharing
|
|
||||||
httpFs := Sharer(netDbDir, c.String("share-password"))
|
httpFs := Sharer(netDbDir, c.String("share-password"))
|
||||||
// Initialize I2P garlic routing for hidden service hosting
|
|
||||||
garlic, err := onramp.NewGarlic("reseed", c.String("samaddr"), onramp.OPT_WIDE)
|
garlic, err := onramp.NewGarlic("reseed", c.String("samaddr"), onramp.OPT_WIDE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer garlic.Close()
|
|
||||||
|
|
||||||
// Create I2P listener for incoming connections
|
|
||||||
garlicListener, err := garlic.Listen()
|
garlicListener, err := garlic.Listen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer garlicListener.Close()
|
|
||||||
|
|
||||||
// Start HTTP server over I2P network
|
|
||||||
return http.Serve(garlicListener, httpFs)
|
return http.Serve(garlicListener, httpFs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// walker creates a tar archive of all files in the specified netDb directory.
|
|
||||||
// This function recursively traverses the directory structure and packages all router
|
|
||||||
// information files into a compressed tar format for efficient network transfer.
|
|
||||||
func walker(netDbDir string) (*bytes.Buffer, error) {
|
func walker(netDbDir string) (*bytes.Buffer, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
// Create tar writer for archive creation
|
|
||||||
tw := tar.NewWriter(&buf)
|
tw := tar.NewWriter(&buf)
|
||||||
walkFn := func(path string, info os.FileInfo, err error) error {
|
walkFn := func(path string, info os.FileInfo, err error) error {
|
||||||
// Handle filesystem errors during directory traversal
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Skip directories, only process regular files
|
|
||||||
if info.Mode().IsDir() {
|
if info.Mode().IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Calculate relative path within netDb directory
|
|
||||||
new_path := path[len(netDbDir):]
|
new_path := path[len(netDbDir):]
|
||||||
if len(new_path) == 0 {
|
if len(new_path) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Open file for reading into tar archive
|
|
||||||
fr, err := os.Open(path)
|
fr, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer fr.Close()
|
defer fr.Close()
|
||||||
if h, err := tar.FileInfoHeader(info, new_path); err != nil {
|
if h, err := tar.FileInfoHeader(info, new_path); err != nil {
|
||||||
lgr.WithError(err).Fatal("Fatal error in share")
|
log.Fatalln(err)
|
||||||
} else {
|
} else {
|
||||||
h.Name = new_path
|
h.Name = new_path
|
||||||
if err = tw.WriteHeader(h); err != nil {
|
if err = tw.WriteHeader(h); err != nil {
|
||||||
lgr.WithError(err).Fatal("Fatal error in share")
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(tw, fr); err != nil {
|
if _, err := io.Copy(tw, fr); err != nil {
|
||||||
lgr.WithError(err).Fatal("Fatal error in share")
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,124 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewShareCommand(t *testing.T) {
|
|
||||||
cmd := NewShareCommand()
|
|
||||||
if cmd == nil {
|
|
||||||
t.Fatal("NewShareCommand() returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Name != "share" {
|
|
||||||
t.Errorf("Expected command name 'share', got %s", cmd.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Action == nil {
|
|
||||||
t.Error("Command action should not be nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSharer(t *testing.T) {
|
|
||||||
// Create temporary directory for test
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Create a test file in the netdb directory
|
|
||||||
testFile := filepath.Join(tempDir, "routerInfo-test.dat")
|
|
||||||
err = os.WriteFile(testFile, []byte("test router info data"), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
password := "testpassword"
|
|
||||||
sharer := Sharer(tempDir, password)
|
|
||||||
|
|
||||||
if sharer == nil {
|
|
||||||
t.Fatal("Sharer() returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that it implements http.Handler
|
|
||||||
var _ http.Handler = sharer
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSharer_ServeHTTP(t *testing.T) {
|
|
||||||
// Create temporary directory for test
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
password := "testpassword"
|
|
||||||
sharer := Sharer(tempDir, password)
|
|
||||||
|
|
||||||
// This test verifies the sharer can be created without panicking
|
|
||||||
// Full HTTP testing would require setting up SAM/I2P which is complex
|
|
||||||
if sharer.Password != password {
|
|
||||||
t.Errorf("Expected password %s, got %s", password, sharer.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sharer.Path != tempDir {
|
|
||||||
t.Errorf("Expected path %s, got %s", tempDir, sharer.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWalker(t *testing.T) {
|
|
||||||
// Create temporary directory with test files
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_walker_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Create test files
|
|
||||||
testFile1 := filepath.Join(tempDir, "routerInfo-test1.dat")
|
|
||||||
testFile2 := filepath.Join(tempDir, "routerInfo-test2.dat")
|
|
||||||
|
|
||||||
err = os.WriteFile(testFile1, []byte("test router info 1"), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test file 1: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(testFile2, []byte("test router info 2"), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test file 2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test walker function
|
|
||||||
result, err := walker(tempDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("walker() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result == nil {
|
|
||||||
t.Fatal("walker() returned nil buffer")
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Len() == 0 {
|
|
||||||
t.Error("walker() returned empty buffer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestShareActionResourceCleanup verifies that resources are properly cleaned up
|
|
||||||
// This is a basic test that can't fully test the I2P functionality but ensures
|
|
||||||
// the command structure is correct
|
|
||||||
func TestShareActionResourceCleanup(t *testing.T) {
|
|
||||||
// This test verifies the function signature and basic setup
|
|
||||||
// Full testing would require a mock SAM interface
|
|
||||||
|
|
||||||
// Skip if running in CI or without I2P SAM available
|
|
||||||
t.Skip("Skipping integration test - requires I2P SAM interface")
|
|
||||||
|
|
||||||
// If we had a mock SAM interface, we would test:
|
|
||||||
// 1. That defer statements are called in correct order
|
|
||||||
// 2. That resources are properly released on error paths
|
|
||||||
// 3. That the server can start and stop cleanly
|
|
||||||
}
|
|
370
cmd/utils.go
370
cmd/utils.go
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -17,8 +18,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"i2pgit.org/go-i2p/reseed-tools/reseed"
|
"i2pgit.org/idk/reseed-tools/reseed"
|
||||||
"i2pgit.org/go-i2p/reseed-tools/su3"
|
"i2pgit.org/idk/reseed-tools/su3"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/certcrypto"
|
"github.com/go-acme/lego/v4/certcrypto"
|
||||||
"github.com/go-acme/lego/v4/certificate"
|
"github.com/go-acme/lego/v4/certificate"
|
||||||
@@ -31,51 +32,57 @@ import (
|
|||||||
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
|
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
|
||||||
privPem, err := ioutil.ReadFile(path)
|
privPem, err := ioutil.ReadFile(path)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).WithField("key_path", path).Error("Failed to read private key file")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
privDer, _ := pem.Decode(privPem)
|
privDer, _ := pem.Decode(privPem)
|
||||||
privKey, err := x509.ParsePKCS1PrivateKey(privDer.Bytes)
|
privKey, err := x509.ParsePKCS1PrivateKey(privDer.Bytes)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).WithField("key_path", path).Error("Failed to parse private key")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return privKey, nil
|
return privKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MyUser struct and methods moved to myuser.go
|
// Taken directly from the lego example, since we need very minimal support
|
||||||
|
// https://go-acme.github.io/lego/usage/library/
|
||||||
|
type MyUser struct {
|
||||||
|
Email string
|
||||||
|
Registration *registration.Resource
|
||||||
|
key crypto.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MyUser) GetEmail() string {
|
||||||
|
return u.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u MyUser) GetRegistration() *registration.Resource {
|
||||||
|
return u.Registration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
|
||||||
|
return u.key
|
||||||
|
}
|
||||||
|
|
||||||
// signerFile creates a filename-safe version of a signer ID.
|
|
||||||
// This function provides consistent filename generation across the cmd package.
|
|
||||||
// Moved from: inline implementations
|
|
||||||
func signerFile(signerID string) string {
|
func signerFile(signerID string) string {
|
||||||
return strings.Replace(signerID, "@", "_at_", 1)
|
return strings.Replace(signerID, "@", "_at_", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOrNewSigningCert(signerKey *string, signerID string, auto bool) (*rsa.PrivateKey, error) {
|
func getOrNewSigningCert(signerKey *string, signerID string, auto bool) (*rsa.PrivateKey, error) {
|
||||||
// Check if signing key file exists before attempting to load
|
|
||||||
if _, err := os.Stat(*signerKey); nil != err {
|
if _, err := os.Stat(*signerKey); nil != err {
|
||||||
lgr.WithError(err).WithField("signer_key", *signerKey).WithField("signer_id", signerID).Debug("Signing key file not found, prompting for generation")
|
|
||||||
fmt.Printf("Unable to read signing key '%s'\n", *signerKey)
|
fmt.Printf("Unable to read signing key '%s'\n", *signerKey)
|
||||||
// Prompt user for key generation in interactive mode
|
|
||||||
if !auto {
|
if !auto {
|
||||||
fmt.Printf("Would you like to generate a new signing key for %s? (y or n): ", signerID)
|
fmt.Printf("Would you like to generate a new signing key for %s? (y or n): ", signerID)
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
input, _ := reader.ReadString('\n')
|
input, _ := reader.ReadString('\n')
|
||||||
if []byte(input)[0] != 'y' {
|
if []byte(input)[0] != 'y' {
|
||||||
lgr.WithField("signer_id", signerID).Error("User declined to generate signing key")
|
return nil, fmt.Errorf("A signing key is required")
|
||||||
return nil, fmt.Errorf("a signing key is required")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Generate new signing certificate if user confirmed or auto mode
|
|
||||||
if err := createSigningCertificate(signerID); nil != err {
|
if err := createSigningCertificate(signerID); nil != err {
|
||||||
lgr.WithError(err).WithField("signer_id", signerID).Error("Failed to create signing certificate")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update key path to point to newly generated certificate
|
|
||||||
*signerKey = signerFile(signerID) + ".pem"
|
*signerKey = signerFile(signerID) + ".pem"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,32 +90,8 @@ func getOrNewSigningCert(signerKey *string, signerID string, auto bool) (*rsa.Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkUseAcmeCert(tlsHost, signer, cadirurl string, tlsCert, tlsKey *string, auto bool) error {
|
func checkUseAcmeCert(tlsHost, signer, cadirurl string, tlsCert, tlsKey *string, auto bool) error {
|
||||||
// Check if certificate files exist and handle missing files
|
|
||||||
needsNewCert, err := checkAcmeCertificateFiles(tlsCert, tlsKey, tlsHost, auto)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If files exist, check if certificate needs renewal
|
|
||||||
if !needsNewCert {
|
|
||||||
shouldRenew, err := checkAcmeCertificateRenewal(tlsCert, tlsKey, tlsHost, signer, cadirurl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !shouldRenew {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new ACME certificate
|
|
||||||
return generateNewAcmeCertificate(tlsHost, signer, cadirurl, tlsCert, tlsKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAcmeCertificateFiles verifies certificate file existence and prompts for generation if needed.
|
|
||||||
func checkAcmeCertificateFiles(tlsCert, tlsKey *string, tlsHost string, auto bool) (bool, error) {
|
|
||||||
_, certErr := os.Stat(*tlsCert)
|
_, certErr := os.Stat(*tlsCert)
|
||||||
_, keyErr := os.Stat(*tlsKey)
|
_, keyErr := os.Stat(*tlsKey)
|
||||||
|
|
||||||
if certErr != nil || keyErr != nil {
|
if certErr != nil || keyErr != nil {
|
||||||
if certErr != nil {
|
if certErr != nil {
|
||||||
fmt.Printf("Unable to read TLS certificate '%s'\n", *tlsCert)
|
fmt.Printf("Unable to read TLS certificate '%s'\n", *tlsCert)
|
||||||
@@ -123,109 +106,73 @@ func checkAcmeCertificateFiles(tlsCert, tlsKey *string, tlsHost string, auto boo
|
|||||||
input, _ := reader.ReadString('\n')
|
input, _ := reader.ReadString('\n')
|
||||||
if []byte(input)[0] != 'y' {
|
if []byte(input)[0] != 'y' {
|
||||||
fmt.Println("Continuing without TLS")
|
fmt.Println("Continuing without TLS")
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true, nil
|
} else {
|
||||||
}
|
TLSConfig := &tls.Config{}
|
||||||
|
TLSConfig.NextProtos = []string{"http/1.1"}
|
||||||
return false, nil
|
TLSConfig.Certificates = make([]tls.Certificate, 1)
|
||||||
}
|
var err error
|
||||||
|
TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(*tlsCert, *tlsKey)
|
||||||
// checkAcmeCertificateRenewal loads existing certificate and checks if renewal is needed.
|
|
||||||
func checkAcmeCertificateRenewal(tlsCert, tlsKey *string, tlsHost, signer, cadirurl string) (bool, error) {
|
|
||||||
tlsConfig := &tls.Config{}
|
|
||||||
tlsConfig.NextProtos = []string{"http/1.1"}
|
|
||||||
tlsConfig.Certificates = make([]tls.Certificate, 1)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(*tlsCert, *tlsKey)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the certificate to populate the Leaf field if it's nil
|
|
||||||
if tlsConfig.Certificates[0].Leaf == nil && len(tlsConfig.Certificates[0].Certificate) > 0 {
|
|
||||||
cert, err := x509.ParseCertificate(tlsConfig.Certificates[0].Certificate[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to parse certificate: %w", err)
|
return err
|
||||||
|
}
|
||||||
|
if time.Now().Sub(TLSConfig.Certificates[0].Leaf.NotAfter) < (time.Hour * 48) {
|
||||||
|
ecder, err := ioutil.ReadFile(tlsHost + signer + ".acme.key")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
privateKey, err := x509.ParseECPrivateKey(ecder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user := MyUser{
|
||||||
|
Email: signer,
|
||||||
|
key: privateKey,
|
||||||
|
}
|
||||||
|
config := lego.NewConfig(&user)
|
||||||
|
config.CADirURL = cadirurl
|
||||||
|
config.Certificate.KeyType = certcrypto.RSA2048
|
||||||
|
client, err := lego.NewClient(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
renewAcmeIssuedCert(client, user, tlsHost, tlsCert, tlsKey)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
tlsConfig.Certificates[0].Leaf = cert
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if certificate expires within 48 hours (time until expiration < 48 hours)
|
|
||||||
if tlsConfig.Certificates[0].Leaf != nil && time.Until(tlsConfig.Certificates[0].Leaf.NotAfter) < (time.Hour*48) {
|
|
||||||
return renewExistingAcmeCertificate(tlsHost, signer, cadirurl, tlsCert, tlsKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// renewExistingAcmeCertificate loads existing ACME key and renews the certificate.
|
|
||||||
func renewExistingAcmeCertificate(tlsHost, signer, cadirurl string, tlsCert, tlsKey *string) (bool, error) {
|
|
||||||
ecder, err := ioutil.ReadFile(tlsHost + signer + ".acme.key")
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, err := x509.ParseECPrivateKey(ecder)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := NewMyUser(signer, privateKey)
|
|
||||||
config := lego.NewConfig(user)
|
|
||||||
config.CADirURL = cadirurl
|
|
||||||
config.Certificate.KeyType = certcrypto.RSA2048
|
|
||||||
|
|
||||||
client, err := lego.NewClient(config)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = renewAcmeIssuedCert(client, *user, tlsHost, tlsCert, tlsKey)
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateNewAcmeCertificate creates a new ACME private key and obtains a certificate.
|
|
||||||
func generateNewAcmeCertificate(tlsHost, signer, cadirurl string, tlsCert, tlsKey *string) error {
|
|
||||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := saveAcmePrivateKey(privateKey, tlsHost, signer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := NewMyUser(signer, privateKey)
|
|
||||||
config := lego.NewConfig(user)
|
|
||||||
config.CADirURL = cadirurl
|
|
||||||
config.Certificate.KeyType = certcrypto.RSA2048
|
|
||||||
|
|
||||||
client, err := lego.NewClient(config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newAcmeIssuedCert(client, *user, tlsHost, tlsCert, tlsKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveAcmePrivateKey marshals and saves the ACME private key to disk.
|
|
||||||
func saveAcmePrivateKey(privateKey *ecdsa.PrivateKey, tlsHost, signer string) error {
|
|
||||||
ecder, err := x509.MarshalECPrivateKey(privateKey)
|
ecder, err := x509.MarshalECPrivateKey(privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := tlsHost + signer + ".acme.key"
|
filename := tlsHost + signer + ".acme.key"
|
||||||
keypem, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
keypem, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer keypem.Close()
|
defer keypem.Close()
|
||||||
|
err = pem.Encode(keypem, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ecder})
|
||||||
return pem.Encode(keypem, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ecder})
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user := MyUser{
|
||||||
|
Email: signer,
|
||||||
|
key: privateKey,
|
||||||
|
}
|
||||||
|
config := lego.NewConfig(&user)
|
||||||
|
config.CADirURL = cadirurl
|
||||||
|
config.Certificate.KeyType = certcrypto.RSA2048
|
||||||
|
client, err := lego.NewClient(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return newAcmeIssuedCert(client, user, tlsHost, tlsCert, tlsKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renewAcmeIssuedCert(client *lego.Client, user MyUser, tlsHost string, tlsCert, tlsKey *string) error {
|
func renewAcmeIssuedCert(client *lego.Client, user MyUser, tlsHost string, tlsCert, tlsKey *string) error {
|
||||||
@@ -333,103 +280,51 @@ func checkOrNewTLSCert(tlsHost string, tlsCert, tlsKey *string, auto bool) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createSigningCertificate generates a new RSA private key and self-signed certificate for SU3 signing.
|
|
||||||
// This function creates the cryptographic materials needed to sign SU3 files for distribution
|
|
||||||
// over the I2P network. The generated certificate is valid for 10 years and uses 4096-bit RSA keys.
|
|
||||||
func createSigningCertificate(signerID string) error {
|
func createSigningCertificate(signerID string) error {
|
||||||
// Generate 4096-bit RSA private key for strong cryptographic security
|
// generate private key
|
||||||
signerKey, err := generateSigningPrivateKey()
|
fmt.Println("Generating signing keys. This may take a minute...")
|
||||||
|
signerKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create self-signed certificate using SU3 certificate standards
|
|
||||||
signerCert, err := su3.NewSigningCertificate(signerID, signerKey)
|
signerCert, err := su3.NewSigningCertificate(signerID, signerKey)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save certificate to disk in PEM format for verification use
|
// save cert
|
||||||
if err := saveSigningCertificateFile(signerID, signerCert); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save signing private key in PKCS#1 PEM format with certificate bundle
|
|
||||||
if err := saveSigningPrivateKeyFile(signerID, signerKey, signerCert); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate and save Certificate Revocation List (CRL)
|
|
||||||
if err := generateAndSaveSigningCRL(signerID, signerKey, signerCert); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateSigningPrivateKey creates a new 4096-bit RSA private key for SU3 signing.
|
|
||||||
// Returns the generated private key or an error if key generation fails.
|
|
||||||
func generateSigningPrivateKey() (*rsa.PrivateKey, error) {
|
|
||||||
fmt.Println("Generating signing keys. This may take a minute...")
|
|
||||||
signerKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return signerKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveSigningCertificateFile saves the signing certificate to disk in PEM format.
|
|
||||||
// The certificate is saved as <signerID>.crt for verification use.
|
|
||||||
func saveSigningCertificateFile(signerID string, signerCert []byte) error {
|
|
||||||
certFile := signerFile(signerID) + ".crt"
|
certFile := signerFile(signerID) + ".crt"
|
||||||
certOut, err := os.Create(certFile)
|
certOut, err := os.Create(certFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open %s for writing: %v", certFile, err)
|
return fmt.Errorf("failed to open %s for writing: %v", certFile, err)
|
||||||
}
|
}
|
||||||
defer certOut.Close()
|
|
||||||
|
|
||||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: signerCert})
|
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: signerCert})
|
||||||
|
certOut.Close()
|
||||||
fmt.Println("\tSigning certificate saved to:", certFile)
|
fmt.Println("\tSigning certificate saved to:", certFile)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveSigningPrivateKeyFile saves the signing private key in PKCS#1 PEM format with certificate bundle.
|
// save signing private key
|
||||||
// The private key is saved as <signerID>.pem with the certificate included for convenience.
|
|
||||||
func saveSigningPrivateKeyFile(signerID string, signerKey *rsa.PrivateKey, signerCert []byte) error {
|
|
||||||
privFile := signerFile(signerID) + ".pem"
|
privFile := signerFile(signerID) + ".pem"
|
||||||
keyOut, err := os.OpenFile(privFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
keyOut, err := os.OpenFile(privFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open %s for writing: %v", privFile, err)
|
return fmt.Errorf("failed to open %s for writing: %v", privFile, err)
|
||||||
}
|
}
|
||||||
defer keyOut.Close()
|
|
||||||
|
|
||||||
// Write RSA private key in PKCS#1 format
|
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(signerKey)})
|
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(signerKey)})
|
||||||
|
|
||||||
// Include certificate in the key file for convenience
|
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "CERTIFICATE", Bytes: signerCert})
|
pem.Encode(keyOut, &pem.Block{Type: "CERTIFICATE", Bytes: signerCert})
|
||||||
|
keyOut.Close()
|
||||||
fmt.Println("\tSigning private key saved to:", privFile)
|
fmt.Println("\tSigning private key saved to:", privFile)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateAndSaveSigningCRL generates and saves a Certificate Revocation List (CRL) for the signing certificate.
|
// CRL
|
||||||
// The CRL is saved as <signerID>.crl and includes the certificate as revoked for testing purposes.
|
|
||||||
func generateAndSaveSigningCRL(signerID string, signerKey *rsa.PrivateKey, signerCert []byte) error {
|
|
||||||
crlFile := signerFile(signerID) + ".crl"
|
crlFile := signerFile(signerID) + ".crl"
|
||||||
crlOut, err := os.OpenFile(crlFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
crlOut, err := os.OpenFile(crlFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open %s for writing: %s", crlFile, err)
|
return fmt.Errorf("failed to open %s for writing: %s", crlFile, err)
|
||||||
}
|
}
|
||||||
defer crlOut.Close()
|
|
||||||
|
|
||||||
// Parse the certificate to extract information for CRL
|
|
||||||
crlcert, err := x509.ParseCertificate(signerCert)
|
crlcert, err := x509.ParseCertificate(signerCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("certificate with unknown critical extension was not parsed: %s", err)
|
return fmt.Errorf("Certificate with unknown critical extension was not parsed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create revoked certificate entry for testing purposes
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
revokedCerts := []pkix.RevokedCertificate{
|
revokedCerts := []pkix.RevokedCertificate{
|
||||||
{
|
{
|
||||||
@@ -438,20 +333,18 @@ func generateAndSaveSigningCRL(signerID string, signerKey *rsa.PrivateKey, signe
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate CRL bytes
|
|
||||||
crlBytes, err := crlcert.CreateCRL(rand.Reader, signerKey, revokedCerts, now, now)
|
crlBytes, err := crlcert.CreateCRL(rand.Reader, signerKey, revokedCerts, now, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating CRL: %s", err)
|
return fmt.Errorf("error creating CRL: %s", err)
|
||||||
}
|
}
|
||||||
|
_, err = x509.ParseDERCRL(crlBytes)
|
||||||
// Validate CRL by parsing it
|
if err != nil {
|
||||||
if _, err := x509.ParseDERCRL(crlBytes); err != nil {
|
|
||||||
return fmt.Errorf("error reparsing CRL: %s", err)
|
return fmt.Errorf("error reparsing CRL: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save CRL to file
|
|
||||||
pem.Encode(crlOut, &pem.Block{Type: "X509 CRL", Bytes: crlBytes})
|
pem.Encode(crlOut, &pem.Block{Type: "X509 CRL", Bytes: crlBytes})
|
||||||
|
crlOut.Close()
|
||||||
fmt.Printf("\tSigning CRL saved to: %s\n", crlFile)
|
fmt.Printf("\tSigning CRL saved to: %s\n", crlFile)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,116 +352,53 @@ func createTLSCertificate(host string) error {
|
|||||||
return CreateTLSCertificate(host)
|
return CreateTLSCertificate(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTLSCertificate generates a new ECDSA private key and self-signed TLS certificate.
|
|
||||||
// This function creates cryptographic materials for HTTPS server operation, using P-384 elliptic
|
|
||||||
// curve cryptography for efficient and secure TLS connections. The certificate is valid for the specified hostname.
|
|
||||||
func CreateTLSCertificate(host string) error {
|
func CreateTLSCertificate(host string) error {
|
||||||
// Generate P-384 ECDSA private key for TLS encryption
|
fmt.Println("Generating TLS keys. This may take a minute...")
|
||||||
priv, err := generateTLSPrivateKey()
|
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create self-signed TLS certificate for the specified hostname
|
|
||||||
tlsCert, err := reseed.NewTLSCertificate(host, priv)
|
tlsCert, err := reseed.NewTLSCertificate(host, priv)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save TLS certificate to disk in PEM format for server use
|
// save the TLS certificate
|
||||||
if err := saveTLSCertificateFile(host, tlsCert); err != nil {
|
certOut, err := os.Create(host + ".crt")
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the TLS private key with EC parameters and certificate bundle
|
|
||||||
if err := saveTLSPrivateKeyFile(host, priv, tlsCert); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate and save Certificate Revocation List (CRL)
|
|
||||||
if err := generateAndSaveTLSCRL(host, priv, tlsCert); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateTLSPrivateKey creates a new P-384 ECDSA private key for TLS encryption.
|
|
||||||
// Returns the generated private key or an error if key generation fails.
|
|
||||||
func generateTLSPrivateKey() (*ecdsa.PrivateKey, error) {
|
|
||||||
fmt.Println("Generating TLS keys. This may take a minute...")
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("failed to open %s for writing: %s", host+".crt", err)
|
||||||
}
|
}
|
||||||
return priv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveTLSCertificateFile saves the TLS certificate to disk in PEM format.
|
|
||||||
// The certificate is saved as <host>.crt for server use.
|
|
||||||
func saveTLSCertificateFile(host string, tlsCert []byte) error {
|
|
||||||
certFile := host + ".crt"
|
|
||||||
certOut, err := os.Create(certFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open %s for writing: %s", certFile, err)
|
|
||||||
}
|
|
||||||
defer certOut.Close()
|
|
||||||
|
|
||||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: tlsCert})
|
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: tlsCert})
|
||||||
fmt.Printf("\tTLS certificate saved to: %s\n", certFile)
|
certOut.Close()
|
||||||
return nil
|
fmt.Printf("\tTLS certificate saved to: %s\n", host+".crt")
|
||||||
}
|
|
||||||
|
|
||||||
// saveTLSPrivateKeyFile saves the TLS private key with EC parameters and certificate bundle.
|
// save the TLS private key
|
||||||
// The private key is saved as <host>.pem with proper EC parameters and certificate included.
|
|
||||||
func saveTLSPrivateKeyFile(host string, priv *ecdsa.PrivateKey, tlsCert []byte) error {
|
|
||||||
privFile := host + ".pem"
|
privFile := host + ".pem"
|
||||||
keyOut, err := os.OpenFile(privFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
keyOut, err := os.OpenFile(privFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open %s for writing: %v", privFile, err)
|
return fmt.Errorf("failed to open %s for writing: %v", privFile, err)
|
||||||
}
|
}
|
||||||
defer keyOut.Close()
|
|
||||||
|
|
||||||
// Encode secp384r1 curve parameters
|
|
||||||
secp384r1, err := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 132, 0, 34}) // http://www.ietf.org/rfc/rfc5480.txt
|
secp384r1, err := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 132, 0, 34}) // http://www.ietf.org/rfc/rfc5480.txt
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal EC parameters: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write EC parameters block
|
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "EC PARAMETERS", Bytes: secp384r1})
|
pem.Encode(keyOut, &pem.Block{Type: "EC PARAMETERS", Bytes: secp384r1})
|
||||||
|
|
||||||
// Marshal and write EC private key
|
|
||||||
ecder, err := x509.MarshalECPrivateKey(priv)
|
ecder, err := x509.MarshalECPrivateKey(priv)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal EC private key: %v", err)
|
|
||||||
}
|
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ecder})
|
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ecder})
|
||||||
|
|
||||||
// Include certificate in the key file
|
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "CERTIFICATE", Bytes: tlsCert})
|
pem.Encode(keyOut, &pem.Block{Type: "CERTIFICATE", Bytes: tlsCert})
|
||||||
|
|
||||||
|
keyOut.Close()
|
||||||
fmt.Printf("\tTLS private key saved to: %s\n", privFile)
|
fmt.Printf("\tTLS private key saved to: %s\n", privFile)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateAndSaveTLSCRL generates and saves a Certificate Revocation List (CRL) for the TLS certificate.
|
// CRL
|
||||||
// The CRL is saved as <host>.crl and includes the certificate as revoked for testing purposes.
|
|
||||||
func generateAndSaveTLSCRL(host string, priv *ecdsa.PrivateKey, tlsCert []byte) error {
|
|
||||||
crlFile := host + ".crl"
|
crlFile := host + ".crl"
|
||||||
crlOut, err := os.OpenFile(crlFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
crlOut, err := os.OpenFile(crlFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open %s for writing: %s", crlFile, err)
|
return fmt.Errorf("failed to open %s for writing: %s", crlFile, err)
|
||||||
}
|
}
|
||||||
defer crlOut.Close()
|
|
||||||
|
|
||||||
// Parse the certificate to extract information for CRL
|
|
||||||
crlcert, err := x509.ParseCertificate(tlsCert)
|
crlcert, err := x509.ParseCertificate(tlsCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("certificate with unknown critical extension was not parsed: %s", err)
|
return fmt.Errorf("Certificate with unknown critical extension was not parsed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create revoked certificate entry for testing purposes
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
revokedCerts := []pkix.RevokedCertificate{
|
revokedCerts := []pkix.RevokedCertificate{
|
||||||
{
|
{
|
||||||
@@ -577,19 +407,17 @@ func generateAndSaveTLSCRL(host string, priv *ecdsa.PrivateKey, tlsCert []byte)
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate CRL bytes
|
|
||||||
crlBytes, err := crlcert.CreateCRL(rand.Reader, priv, revokedCerts, now, now)
|
crlBytes, err := crlcert.CreateCRL(rand.Reader, priv, revokedCerts, now, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating CRL: %s", err)
|
return fmt.Errorf("error creating CRL: %s", err)
|
||||||
}
|
}
|
||||||
|
_, err = x509.ParseDERCRL(crlBytes)
|
||||||
// Validate CRL by parsing it
|
if err != nil {
|
||||||
if _, err := x509.ParseDERCRL(crlBytes); err != nil {
|
|
||||||
return fmt.Errorf("error reparsing CRL: %s", err)
|
return fmt.Errorf("error reparsing CRL: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save CRL to file
|
|
||||||
pem.Encode(crlOut, &pem.Block{Type: "X509 CRL", Bytes: crlBytes})
|
pem.Encode(crlOut, &pem.Block{Type: "X509 CRL", Bytes: crlBytes})
|
||||||
|
crlOut.Close()
|
||||||
fmt.Printf("\tTLS CRL saved to: %s\n", crlFile)
|
fmt.Printf("\tTLS CRL saved to: %s\n", crlFile)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,283 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/pem"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCertificateExpirationLogic(t *testing.T) {
|
|
||||||
// Generate a test RSA key
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
expiresIn time.Duration
|
|
||||||
shouldRenew bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Certificate expires in 24 hours",
|
|
||||||
expiresIn: 24 * time.Hour,
|
|
||||||
shouldRenew: true,
|
|
||||||
description: "Should renew certificate that expires within 48 hours",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Certificate expires in 72 hours",
|
|
||||||
expiresIn: 72 * time.Hour,
|
|
||||||
shouldRenew: false,
|
|
||||||
description: "Should not renew certificate with more than 48 hours remaining",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Certificate expires in 47 hours",
|
|
||||||
expiresIn: 47 * time.Hour,
|
|
||||||
shouldRenew: true,
|
|
||||||
description: "Should renew certificate just under 48 hour threshold",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Certificate expires in 49 hours",
|
|
||||||
expiresIn: 49 * time.Hour,
|
|
||||||
shouldRenew: false,
|
|
||||||
description: "Should not renew certificate just over 48 hour threshold",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// Create a certificate that expires at the specified time
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"Test"},
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(tc.expiresIn),
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the logic that was fixed
|
|
||||||
shouldRenew := time.Until(cert.NotAfter) < (time.Hour * 48)
|
|
||||||
|
|
||||||
if shouldRenew != tc.shouldRenew {
|
|
||||||
t.Errorf("%s: Expected shouldRenew=%v, got %v. %s",
|
|
||||||
tc.name, tc.shouldRenew, shouldRenew, tc.description)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also test that a TLS certificate with this cert would have the same behavior
|
|
||||||
tlsCert := tls.Certificate{
|
|
||||||
Certificate: [][]byte{certDER},
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
Leaf: cert,
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsShouldRenew := time.Until(tlsCert.Leaf.NotAfter) < (time.Hour * 48)
|
|
||||||
if tlsShouldRenew != tc.shouldRenew {
|
|
||||||
t.Errorf("%s: TLS certificate logic mismatch. Expected shouldRenew=%v, got %v",
|
|
||||||
tc.name, tc.shouldRenew, tlsShouldRenew)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOldBuggyLogic(t *testing.T) {
|
|
||||||
// Test to demonstrate that the old buggy logic was incorrect
|
|
||||||
|
|
||||||
// Create a certificate that expires in 24 hours (should be renewed)
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"Test"},
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(24 * time.Hour), // Expires in 24 hours
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Old buggy logic (commented out to show what was wrong)
|
|
||||||
// oldLogic := time.Now().Sub(cert.NotAfter) < (time.Hour * 48)
|
|
||||||
|
|
||||||
// New correct logic
|
|
||||||
newLogic := time.Until(cert.NotAfter) < (time.Hour * 48)
|
|
||||||
|
|
||||||
// For a certificate expiring in 24 hours:
|
|
||||||
// - Old logic would be: time.Now().Sub(futureTime) = negative value < 48 hours = false (wrong!)
|
|
||||||
// - New logic would be: time.Until(futureTime) = 24 hours < 48 hours = true (correct!)
|
|
||||||
|
|
||||||
if !newLogic {
|
|
||||||
t.Error("New logic should indicate renewal needed for certificate expiring in 24 hours")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for Bug #1: Nil Pointer Dereference in TLS Certificate Renewal
|
|
||||||
func TestNilPointerDereferenceTLSRenewal(t *testing.T) {
|
|
||||||
// Create a temporary certificate and key file
|
|
||||||
cert, key, err := generateTestCertificate()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create temporary files
|
|
||||||
certFile := "test-cert.pem"
|
|
||||||
keyFile := "test-key.pem"
|
|
||||||
|
|
||||||
// Write certificate and key to files
|
|
||||||
if err := os.WriteFile(certFile, cert, 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to write cert file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(certFile)
|
|
||||||
|
|
||||||
if err := os.WriteFile(keyFile, key, 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to write key file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(keyFile)
|
|
||||||
|
|
||||||
// Create a minimal test to reproduce the exact nil pointer issue
|
|
||||||
// This directly tests what happens when tls.LoadX509KeyPair is used
|
|
||||||
// and then Leaf is accessed without checking if it's nil
|
|
||||||
tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load X509 key pair: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This demonstrates the bug: tlsCert.Leaf is nil after LoadX509KeyPair
|
|
||||||
if tlsCert.Leaf == nil {
|
|
||||||
t.Log("Confirmed: tlsCert.Leaf is nil after LoadX509KeyPair - this causes the bug")
|
|
||||||
}
|
|
||||||
|
|
||||||
// This would panic with nil pointer dereference before the fix:
|
|
||||||
// tlsCert.Leaf.NotAfter would panic
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Log("Caught panic accessing tlsCert.Leaf.NotAfter:", r)
|
|
||||||
// This panic is expected before the fix is applied
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// This should reproduce the exact bug from line 147 in utils.go
|
|
||||||
// Before fix: panics with nil pointer dereference
|
|
||||||
// After fix: should handle gracefully
|
|
||||||
if tlsCert.Leaf != nil {
|
|
||||||
_ = time.Until(tlsCert.Leaf.NotAfter) < (time.Hour * 48)
|
|
||||||
t.Log("No panic occurred - fix may be already applied")
|
|
||||||
} else {
|
|
||||||
// This will panic before the fix
|
|
||||||
_ = time.Until(tlsCert.Leaf.NotAfter) < (time.Hour * 48)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateTestCertificate creates a test certificate and key for testing the nil pointer bug
|
|
||||||
func generateTestCertificate() ([]byte, []byte, error) {
|
|
||||||
// Generate private key
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create certificate template - expires in 24 hours to trigger renewal logic
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"Test Org"},
|
|
||||||
Country: []string{"US"},
|
|
||||||
Province: []string{""},
|
|
||||||
Locality: []string{"Test City"},
|
|
||||||
StreetAddress: []string{""},
|
|
||||||
PostalCode: []string{""},
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(24 * time.Hour), // Expires in 24 hours (should trigger renewal)
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
IPAddresses: nil,
|
|
||||||
DNSNames: []string{"test.example.com"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create certificate
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode certificate to PEM
|
|
||||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: certDER,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Encode private key to PEM
|
|
||||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
||||||
})
|
|
||||||
|
|
||||||
return certPEM, keyPEM, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for Bug #1 Fix: Certificate Leaf parsing works correctly
|
|
||||||
func TestCertificateLeafParsingFix(t *testing.T) {
|
|
||||||
cert, key, err := generateTestCertificate()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certFile := "test-cert-fix.pem"
|
|
||||||
keyFile := "test-key-fix.pem"
|
|
||||||
|
|
||||||
if err := os.WriteFile(certFile, cert, 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to write cert file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(certFile)
|
|
||||||
|
|
||||||
if err := os.WriteFile(keyFile, key, 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to write key file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(keyFile)
|
|
||||||
|
|
||||||
// Test the fix: our function should handle nil Leaf gracefully
|
|
||||||
shouldRenew, err := checkAcmeCertificateRenewal(&certFile, &keyFile, "test", "test", "https://acme-v02.api.letsencrypt.org/directory")
|
|
||||||
|
|
||||||
// We expect an error (likely ACME-related), but NOT a panic or nil pointer error
|
|
||||||
if err != nil && (strings.Contains(err.Error(), "runtime error") || strings.Contains(err.Error(), "nil pointer")) {
|
|
||||||
t.Errorf("Fix failed: still getting nil pointer error: %v", err)
|
|
||||||
} else {
|
|
||||||
t.Logf("Fix successful: no nil pointer errors (got: %v, shouldRenew: %v)", err, shouldRenew)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -3,35 +3,30 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"i2pgit.org/go-i2p/reseed-tools/reseed"
|
"i2pgit.org/idk/reseed-tools/reseed"
|
||||||
"i2pgit.org/go-i2p/reseed-tools/su3"
|
"i2pgit.org/idk/reseed-tools/su3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// I2PHome returns the I2P configuration directory path for the current system.
|
|
||||||
// It checks multiple standard locations including environment variables and default
|
|
||||||
// directories to locate I2P configuration files and certificates for SU3 verification.
|
|
||||||
func I2PHome() string {
|
func I2PHome() string {
|
||||||
// Check I2P environment variable first for custom installations
|
|
||||||
envCheck := os.Getenv("I2P")
|
envCheck := os.Getenv("I2P")
|
||||||
if envCheck != "" {
|
if envCheck != "" {
|
||||||
return envCheck
|
return envCheck
|
||||||
}
|
}
|
||||||
// Get current user's home directory for standard I2P paths
|
// get the current user home
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
// Check for i2p-config directory (common on Linux distributions)
|
|
||||||
sysCheck := filepath.Join(usr.HomeDir, "i2p-config")
|
sysCheck := filepath.Join(usr.HomeDir, "i2p-config")
|
||||||
if _, err := os.Stat(sysCheck); nil == err {
|
if _, err := os.Stat(sysCheck); nil == err {
|
||||||
return sysCheck
|
return sysCheck
|
||||||
}
|
}
|
||||||
// Check for standard i2p directory in user home
|
|
||||||
usrCheck := filepath.Join(usr.HomeDir, "i2p")
|
usrCheck := filepath.Join(usr.HomeDir, "i2p")
|
||||||
if _, err := os.Stat(usrCheck); nil == err {
|
if _, err := os.Stat(usrCheck); nil == err {
|
||||||
return usrCheck
|
return usrCheck
|
||||||
@@ -39,9 +34,6 @@ func I2PHome() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSu3VerifyCommand creates a new CLI command for verifying SU3 file signatures.
|
|
||||||
// This command validates the cryptographic integrity of SU3 files using the embedded
|
|
||||||
// certificates and signatures, ensuring files haven't been tampered with during distribution.
|
|
||||||
func NewSu3VerifyCommand() *cli.Command {
|
func NewSu3VerifyCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "verify",
|
Name: "verify",
|
||||||
@@ -92,7 +84,7 @@ func su3VerifyAction(c *cli.Context) error {
|
|||||||
if c.String("signer") != "" {
|
if c.String("signer") != "" {
|
||||||
su3File.SignerID = []byte(c.String("signer"))
|
su3File.SignerID = []byte(c.String("signer"))
|
||||||
}
|
}
|
||||||
lgr.WithField("keystore", absPath).WithField("purpose", reseedDir).WithField("signer", string(su3File.SignerID)).Debug("Using keystore")
|
log.Println("Using keystore:", absPath, "for purpose", reseedDir, "and", string(su3File.SignerID))
|
||||||
|
|
||||||
cert, err := ks.DirReseederCertificate(reseedDir, su3File.SignerID)
|
cert, err := ks.DirReseederCertificate(reseedDir, su3File.SignerID)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
|
@@ -4,18 +4,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"i2pgit.org/go-i2p/reseed-tools/reseed"
|
"i2pgit.org/idk/reseed-tools/reseed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewVersionCommand creates a new CLI command for displaying the reseed-tools version.
|
|
||||||
// This command provides version information for troubleshooting and compatibility checking
|
|
||||||
// with other I2P network components and reseed infrastructure.
|
|
||||||
func NewVersionCommand() *cli.Command {
|
func NewVersionCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Usage: "Print the version number of reseed-tools",
|
Usage: "Print the version number of reseed-tools",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
// Print the current version from reseed package constants
|
|
||||||
fmt.Printf("%s\n", reseed.Version)
|
fmt.Printf("%s\n", reseed.Version)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@@ -14,7 +14,7 @@ included, apply on [i2pforum.i2p](http://i2pforum.i2p).
|
|||||||
|
|
||||||
`go`, `git`, and optionally `make` are required to build the project.
|
`go`, `git`, and optionally `make` are required to build the project.
|
||||||
Precompiled binaries for most platforms are available at my github mirror
|
Precompiled binaries for most platforms are available at my github mirror
|
||||||
https://github.com/go-i2p/reseed-tools.
|
https://github.com/eyedeekay/i2p-tools-1.
|
||||||
|
|
||||||
In order to install the build-dependencies on Ubuntu or Debian, you may use:
|
In order to install the build-dependencies on Ubuntu or Debian, you may use:
|
||||||
|
|
||||||
|
@@ -98,7 +98,12 @@
|
|||||||
<h3>
|
<h3>
|
||||||
Without a webserver, standalone, automatic OnionV3 with TLS support
|
Without a webserver, standalone, automatic OnionV3 with TLS support
|
||||||
</h3>
|
</h3>
|
||||||
<pre><code>./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p
|
<pre><code>./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p --p2p
|
||||||
|
</code></pre>
|
||||||
|
<h3>
|
||||||
|
Without a webserver, standalone, serve P2P with LibP2P
|
||||||
|
</h3>
|
||||||
|
<pre><code>./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --p2p
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3>
|
<h3>
|
||||||
Without a webserver, standalone, in-network reseed
|
Without a webserver, standalone, in-network reseed
|
||||||
@@ -111,9 +116,9 @@
|
|||||||
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3>
|
<h3>
|
||||||
Without a webserver, standalone, Regular TLS, OnionV3 with TLS
|
Without a webserver, standalone, Regular TLS, OnionV3 with TLS, and LibP2P
|
||||||
</h3>
|
</h3>
|
||||||
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --p2p
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<div id="sourcecode">
|
<div id="sourcecode">
|
||||||
<span id="sourcehead">
|
<span id="sourcehead">
|
||||||
|
@@ -4,7 +4,13 @@
|
|||||||
### Without a webserver, standalone, automatic OnionV3 with TLS support
|
### Without a webserver, standalone, automatic OnionV3 with TLS support
|
||||||
|
|
||||||
```
|
```
|
||||||
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p
|
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p --p2p
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without a webserver, standalone, serve P2P with LibP2P
|
||||||
|
|
||||||
|
```
|
||||||
|
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --p2p
|
||||||
```
|
```
|
||||||
|
|
||||||
### Without a webserver, standalone, in-network reseed
|
### Without a webserver, standalone, in-network reseed
|
||||||
@@ -19,8 +25,8 @@
|
|||||||
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
||||||
```
|
```
|
||||||
|
|
||||||
### Without a webserver, standalone, Regular TLS, OnionV3 with TLS
|
### Without a webserver, standalone, Regular TLS, OnionV3 with TLS, and LibP2P
|
||||||
|
|
||||||
```
|
```
|
||||||
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --p2p
|
||||||
```
|
```
|
||||||
|
@@ -101,8 +101,8 @@
|
|||||||
http://idk.i2p/reseed-tools/
|
http://idk.i2p/reseed-tools/
|
||||||
</a>
|
</a>
|
||||||
and via the github mirror at
|
and via the github mirror at
|
||||||
<a href="https://github.com/go-i2p/reseed-tools/releases">
|
<a href="https://github.com/eyedeekay/reseed-tools/releases">
|
||||||
https://github.com/go-i2p/reseed-tools/releases
|
https://github.com/eyedeekay/reseed-tools/releases
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
These can be installed by adding them on the
|
These can be installed by adding them on the
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# Plugin install URL's
|
# Plugin install URL's
|
||||||
|
|
||||||
Plugin releases are available inside of i2p at http://idk.i2p/reseed-tools/
|
Plugin releases are available inside of i2p at http://idk.i2p/reseed-tools/
|
||||||
and via the github mirror at https://github.com/go-i2p/reseed-tools/releases.
|
and via the github mirror at https://github.com/eyedeekay/reseed-tools/releases.
|
||||||
These can be installed by adding them on the
|
These can be installed by adding them on the
|
||||||
[http://127.0.0.1:7657/configplugins](http://127.0.0.1:7657/configplugins).
|
[http://127.0.0.1:7657/configplugins](http://127.0.0.1:7657/configplugins).
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ system service.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
||||||
wget https://github.com/go-i2p/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
wget https://github.com/eyedeekay/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
||||||
# Obtain the checksum from the release web page
|
# Obtain the checksum from the release web page
|
||||||
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
||||||
sha256sums -c SHA256SUMS
|
sha256sums -c SHA256SUMS
|
||||||
|
@@ -196,7 +196,7 @@
|
|||||||
https://i2pgit.org/idk/reseed-tools)
|
https://i2pgit.org/idk/reseed-tools)
|
||||||
</a>
|
</a>
|
||||||
or
|
or
|
||||||
<a href="https://github.com/go-i2p/reseed-tools">
|
<a href="https://github.com/eyedeekay/reseed-tools">
|
||||||
github
|
github
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
@@ -42,7 +42,7 @@ To automate this process using an ACME CA, like Let's Encrypt, you can use the `
|
|||||||
Be sure to change the `--acmeserver` option in order to use a **production** ACME server, as
|
Be sure to change the `--acmeserver` option in order to use a **production** ACME server, as
|
||||||
the software defaults to a **staging** ACME server for testing purposes.
|
the software defaults to a **staging** ACME server for testing purposes.
|
||||||
|
|
||||||
This functionality is new and may have issues. Please file bug reports at (i2pgit)[https://i2pgit.org/idk/reseed-tools) or [github](https://github.com/go-i2p/reseed-tools).
|
This functionality is new and may have issues. Please file bug reports at (i2pgit)[https://i2pgit.org/idk/reseed-tools) or [github](https://github.com/eyedeekay/reseed-tools).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
||||||
|
@@ -131,7 +131,7 @@
|
|||||||
system service.
|
system service.
|
||||||
</p>
|
</p>
|
||||||
<pre><code class="language-sh">
|
<pre><code class="language-sh">
|
||||||
wget https://github.com/go-i2p/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
wget https://github.com/eyedeekay/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
||||||
# Obtain the checksum from the release web page
|
# Obtain the checksum from the release web page
|
||||||
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
||||||
sha256sums -c SHA256SUMS
|
sha256sums -c SHA256SUMS
|
||||||
|
@@ -98,7 +98,12 @@
|
|||||||
<h3>
|
<h3>
|
||||||
Without a webserver, standalone, automatic OnionV3 with TLS support
|
Without a webserver, standalone, automatic OnionV3 with TLS support
|
||||||
</h3>
|
</h3>
|
||||||
<pre><code>./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p
|
<pre><code>./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p --p2p
|
||||||
|
</code></pre>
|
||||||
|
<h3>
|
||||||
|
Without a webserver, standalone, serve P2P with LibP2P
|
||||||
|
</h3>
|
||||||
|
<pre><code>./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --p2p
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3>
|
<h3>
|
||||||
Without a webserver, standalone, in-network reseed
|
Without a webserver, standalone, in-network reseed
|
||||||
@@ -111,9 +116,9 @@
|
|||||||
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3>
|
<h3>
|
||||||
Without a webserver, standalone, Regular TLS, OnionV3 with TLS
|
Without a webserver, standalone, Regular TLS, OnionV3 with TLS, and LibP2P
|
||||||
</h3>
|
</h3>
|
||||||
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
<pre><code>./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --p2p
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<div id="sourcecode">
|
<div id="sourcecode">
|
||||||
<span id="sourcehead">
|
<span id="sourcehead">
|
||||||
|
@@ -4,7 +4,13 @@
|
|||||||
### Without a webserver, standalone, automatic OnionV3 with TLS support
|
### Without a webserver, standalone, automatic OnionV3 with TLS support
|
||||||
|
|
||||||
```
|
```
|
||||||
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p
|
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --i2p --p2p
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without a webserver, standalone, serve P2P with LibP2P
|
||||||
|
|
||||||
|
```
|
||||||
|
./reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --p2p
|
||||||
```
|
```
|
||||||
|
|
||||||
### Without a webserver, standalone, in-network reseed
|
### Without a webserver, standalone, in-network reseed
|
||||||
@@ -19,8 +25,8 @@
|
|||||||
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
||||||
```
|
```
|
||||||
|
|
||||||
### Without a webserver, standalone, Regular TLS, OnionV3 with TLS
|
### Without a webserver, standalone, Regular TLS, OnionV3 with TLS, and LibP2P
|
||||||
|
|
||||||
```
|
```
|
||||||
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion
|
./reseed-tools reseed --tlsHost=your-domain.tld --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --onion --p2p
|
||||||
```
|
```
|
||||||
|
@@ -101,8 +101,8 @@
|
|||||||
http://idk.i2p/reseed-tools/
|
http://idk.i2p/reseed-tools/
|
||||||
</a>
|
</a>
|
||||||
and via the github mirror at
|
and via the github mirror at
|
||||||
<a href="https://github.com/go-i2p/reseed-tools/releases">
|
<a href="https://github.com/eyedeekay/reseed-tools/releases">
|
||||||
https://github.com/go-i2p/reseed-tools/releases
|
https://github.com/eyedeekay/reseed-tools/releases
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
These can be installed by adding them on the
|
These can be installed by adding them on the
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# Plugin install URL's
|
# Plugin install URL's
|
||||||
|
|
||||||
Plugin releases are available inside of i2p at http://idk.i2p/reseed-tools/
|
Plugin releases are available inside of i2p at http://idk.i2p/reseed-tools/
|
||||||
and via the github mirror at https://github.com/go-i2p/reseed-tools/releases.
|
and via the github mirror at https://github.com/eyedeekay/reseed-tools/releases.
|
||||||
These can be installed by adding them on the
|
These can be installed by adding them on the
|
||||||
[http://127.0.0.1:7657/configplugins](http://127.0.0.1:7657/configplugins).
|
[http://127.0.0.1:7657/configplugins](http://127.0.0.1:7657/configplugins).
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ system service.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
||||||
wget https://github.com/go-i2p/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
wget https://github.com/eyedeekay/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
||||||
# Obtain the checksum from the release web page
|
# Obtain the checksum from the release web page
|
||||||
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
||||||
sha256sums -c SHA256SUMS
|
sha256sums -c SHA256SUMS
|
||||||
|
@@ -196,7 +196,7 @@
|
|||||||
https://i2pgit.org/idk/reseed-tools)
|
https://i2pgit.org/idk/reseed-tools)
|
||||||
</a>
|
</a>
|
||||||
or
|
or
|
||||||
<a href="https://github.com/go-i2p/reseed-tools">
|
<a href="https://github.com/eyedeekay/reseed-tools">
|
||||||
github
|
github
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
@@ -42,7 +42,7 @@ To automate this process using an ACME CA, like Let's Encrypt, you can use the `
|
|||||||
Be sure to change the `--acmeserver` option in order to use a **production** ACME server, as
|
Be sure to change the `--acmeserver` option in order to use a **production** ACME server, as
|
||||||
the software defaults to a **staging** ACME server for testing purposes.
|
the software defaults to a **staging** ACME server for testing purposes.
|
||||||
|
|
||||||
This functionality is new and may have issues. Please file bug reports at (i2pgit)[https://i2pgit.org/idk/reseed-tools) or [github](https://github.com/go-i2p/reseed-tools).
|
This functionality is new and may have issues. Please file bug reports at (i2pgit)[https://i2pgit.org/idk/reseed-tools) or [github](https://github.com/eyedeekay/reseed-tools).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
||||||
|
@@ -131,7 +131,7 @@
|
|||||||
system service.
|
system service.
|
||||||
</p>
|
</p>
|
||||||
<pre><code class="language-sh">
|
<pre><code class="language-sh">
|
||||||
wget https://github.com/go-i2p/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
wget https://github.com/eyedeekay/reseed-tools/releases/download/v0.2.30/reseed-tools_0.2.30-1_amd64.deb
|
||||||
# Obtain the checksum from the release web page
|
# Obtain the checksum from the release web page
|
||||||
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
echo "38941246e980dfc0456e066f514fc96a4ba25d25a7ef993abd75130770fa4d4d reseed-tools_0.2.30-1_amd64.deb" > SHA256SUMS
|
||||||
sha256sums -c SHA256SUMS
|
sha256sums -c SHA256SUMS
|
||||||
|
47
go.mod
47
go.mod
@@ -1,18 +1,17 @@
|
|||||||
module i2pgit.org/go-i2p/reseed-tools
|
module i2pgit.org/idk/reseed-tools
|
||||||
|
|
||||||
go 1.24.2
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
|
github.com/eyedeekay/checki2cp v0.33.8
|
||||||
github.com/eyedeekay/go-i2pd v0.0.0-20220213070306-9807541b2dfc
|
github.com/eyedeekay/go-i2pd v0.0.0-20220213070306-9807541b2dfc
|
||||||
|
github.com/eyedeekay/i2pkeys v0.33.8
|
||||||
|
github.com/eyedeekay/onramp v0.33.7
|
||||||
|
github.com/eyedeekay/sam3 v0.33.8
|
||||||
github.com/eyedeekay/unembed v0.0.0-20230123014222-9916b121855b
|
github.com/eyedeekay/unembed v0.0.0-20230123014222-9916b121855b
|
||||||
github.com/go-acme/lego/v4 v4.3.1
|
github.com/go-acme/lego/v4 v4.3.1
|
||||||
github.com/go-i2p/checki2cp v0.0.0-20250819201001-7a3f89fafac8
|
github.com/go-i2p/go-i2p v0.0.0-20250130205134-f144c457ba5d
|
||||||
github.com/go-i2p/common v0.0.0-20250819190749-01946d9f7ccf
|
|
||||||
github.com/go-i2p/i2pkeys v0.33.92
|
|
||||||
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c
|
|
||||||
github.com/go-i2p/onramp v0.33.92
|
|
||||||
github.com/go-i2p/sam3 v0.33.92
|
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/justinas/alice v1.2.0
|
github.com/justinas/alice v1.2.0
|
||||||
github.com/otiai10/copy v1.14.0
|
github.com/otiai10/copy v1.14.0
|
||||||
@@ -20,37 +19,7 @@ require (
|
|||||||
github.com/throttled/throttled/v2 v2.7.1
|
github.com/throttled/throttled/v2 v2.7.1
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha
|
github.com/urfave/cli/v3 v3.0.0-alpha
|
||||||
gitlab.com/golang-commonmark/markdown v0.0.0-20191127184510-91b5b3c99c19
|
gitlab.com/golang-commonmark/markdown v0.0.0-20191127184510-91b5b3c99c19
|
||||||
golang.org/x/text v0.26.0
|
golang.org/x/text v0.15.0
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
|
||||||
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
|
||||||
github.com/go-i2p/crypto v0.0.0-20250715200104-0ce55885b9cf // indirect
|
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
|
||||||
github.com/miekg/dns v1.1.40 // indirect
|
|
||||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
|
||||||
github.com/samber/lo v1.51.0 // indirect
|
|
||||||
github.com/samber/oops v1.19.0 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
|
||||||
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 // indirect
|
|
||||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 // indirect
|
|
||||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84 // indirect
|
|
||||||
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f // indirect
|
|
||||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
|
||||||
go.step.sm/crypto v0.67.0 // indirect
|
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
|
||||||
golang.org/x/net v0.41.0 // indirect
|
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//replace github.com/go-i2p/go-i2p => ../../../github.com/go-i2p/go-i2p
|
//replace github.com/go-i2p/go-i2p => ../../../github.com/go-i2p/go-i2p
|
||||||
|
151
go.sum
151
go.sum
@@ -23,8 +23,6 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
|||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
|
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
|
||||||
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||||
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
|
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
|
||||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||||
@@ -41,6 +39,7 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L
|
|||||||
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
|
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
|
||||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
||||||
@@ -86,9 +85,8 @@ github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
|||||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/deepmap/oapi-codegen v1.3.11/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
|
github.com/deepmap/oapi-codegen v1.3.11/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
@@ -97,24 +95,49 @@ github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c
|
|||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/exoscale/egoscale v0.46.0/go.mod h1:mpEXBpROAa/2i5GC0r33rfxG+TxSEka11g1PIXt9+zc=
|
github.com/exoscale/egoscale v0.46.0/go.mod h1:mpEXBpROAa/2i5GC0r33rfxG+TxSEka11g1PIXt9+zc=
|
||||||
|
github.com/eyedeekay/checki2cp v0.33.8 h1:h31UDIuTP7Pv0T332RlRUieTwaNT+LoLPPLOhkwecsk=
|
||||||
|
github.com/eyedeekay/checki2cp v0.33.8/go.mod h1:n40YU2DtJI4iW6H8Wdqma062PI6L2ruVpG8QtsOjYRQ=
|
||||||
|
github.com/eyedeekay/go-i2cp v0.0.0-20190716135428-6d41bed718b0 h1:rnn9OlD/3+tATEZNuiMR1C84O5CX8bZL2qqgttprKrw=
|
||||||
|
github.com/eyedeekay/go-i2cp v0.0.0-20190716135428-6d41bed718b0/go.mod h1:+P0fIhkqIYjo7exMJRTlSteRMbRyHbiBiKw+YlPWk+c=
|
||||||
|
github.com/eyedeekay/go-i2pcontrol v0.1.6/go.mod h1:976YyzS3THPwlBelkp3t1pMzzsjyn96eLaVdhaaSI78=
|
||||||
github.com/eyedeekay/go-i2pd v0.0.0-20220213070306-9807541b2dfc h1:ozp8Cxn9nsFF+p4tMcE63G0Kx+2lEywlCW0EvtISEZg=
|
github.com/eyedeekay/go-i2pd v0.0.0-20220213070306-9807541b2dfc h1:ozp8Cxn9nsFF+p4tMcE63G0Kx+2lEywlCW0EvtISEZg=
|
||||||
github.com/eyedeekay/go-i2pd v0.0.0-20220213070306-9807541b2dfc/go.mod h1:Yg8xCWRLyq0mezPV+xJygBhJCf7wYsIdXbYGQk5tnW8=
|
github.com/eyedeekay/go-i2pd v0.0.0-20220213070306-9807541b2dfc/go.mod h1:Yg8xCWRLyq0mezPV+xJygBhJCf7wYsIdXbYGQk5tnW8=
|
||||||
|
github.com/eyedeekay/goSam v0.32.31-0.20210122211817-f97683379f23/go.mod h1:UgJnih/LpotwKriwVPOEa6yPDM2NDdVrKfLtS5DOLPE=
|
||||||
github.com/eyedeekay/i2pd v0.3.0-1stbinrelease.0.20210702172028-5d01ee95810a/go.mod h1:4qJhWn+yNrWRbqFHhU8kl7JgbcW1hm3PMgvlPlxO3gg=
|
github.com/eyedeekay/i2pd v0.3.0-1stbinrelease.0.20210702172028-5d01ee95810a/go.mod h1:4qJhWn+yNrWRbqFHhU8kl7JgbcW1hm3PMgvlPlxO3gg=
|
||||||
|
github.com/eyedeekay/i2pkeys v0.33.7/go.mod h1:W9KCm9lqZ+Ozwl3dwcgnpPXAML97+I8Jiht7o5A8YBM=
|
||||||
|
github.com/eyedeekay/i2pkeys v0.33.8 h1:f3llyruchFqs1QwCacBYbShArKPpMSSOqo/DVZXcfVs=
|
||||||
|
github.com/eyedeekay/i2pkeys v0.33.8/go.mod h1:W9KCm9lqZ+Ozwl3dwcgnpPXAML97+I8Jiht7o5A8YBM=
|
||||||
|
github.com/eyedeekay/onramp v0.33.7 h1:LkPklut7Apa6CPGdIoOJpyIpzP9H/Jw7RKvrVxEEYEM=
|
||||||
|
github.com/eyedeekay/onramp v0.33.7/go.mod h1:+Dutoc91mCHLJlYNE3Ir6kSfmpEcQA6/RNHnmVVznWg=
|
||||||
|
github.com/eyedeekay/sam3 v0.32.32/go.mod h1:qRA9KIIVxbrHlkj+ZB+OoxFGFgdKeGp1vSgPw26eOVU=
|
||||||
|
github.com/eyedeekay/sam3 v0.33.7/go.mod h1:25cRGEFawSkbiPNSh7vTUIpRtEYLVLg/4J4He6LndAY=
|
||||||
|
github.com/eyedeekay/sam3 v0.33.8 h1:emuSZ4qSyoqc1EDjIBFbJ3GXNHOXw6hjbNp2OqdOpgI=
|
||||||
|
github.com/eyedeekay/sam3 v0.33.8/go.mod h1:ytbwLYLJlW6UA92Ffyc6oioWTKnGeeUMr9CLuJbtqSA=
|
||||||
github.com/eyedeekay/unembed v0.0.0-20230123014222-9916b121855b h1:QyCSwbHpkJtKGvIvHsvvlbDkf7/3a8qUlaa4rEr8myQ=
|
github.com/eyedeekay/unembed v0.0.0-20230123014222-9916b121855b h1:QyCSwbHpkJtKGvIvHsvvlbDkf7/3a8qUlaa4rEr8myQ=
|
||||||
github.com/eyedeekay/unembed v0.0.0-20230123014222-9916b121855b/go.mod h1:A6dZU88muI132XMrmdM0+cc2yIuwmhwgRfyrU54DjPc=
|
github.com/eyedeekay/unembed v0.0.0-20230123014222-9916b121855b/go.mod h1:A6dZU88muI132XMrmdM0+cc2yIuwmhwgRfyrU54DjPc=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
|
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
||||||
github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
|
github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||||
|
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||||
|
github.com/getlantern/go-socks5 v0.0.0-20171114193258-79d4dd3e2db5/go.mod h1:kGHRXch95rnGLHjER/GhhFiHvfnqNz7KqWD9kGfATHY=
|
||||||
|
github.com/getlantern/golog v0.0.0-20201105130739-9586b8bde3a9/go.mod h1:ZyIjgH/1wTCl+B+7yH1DqrWp6MPJqESmwmEQ89ZfhvA=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
|
github.com/getlantern/netx v0.0.0-20190110220209-9912de6f94fd/go.mod h1:wKdY0ikOgzrWSeB9UyBVKPRhjXQ+vTb+BPeJuypUuNE=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20200403153110-8476b16edcd6/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-acme/lego/v4 v4.3.1 h1:rzmg0Gpy25B/exXjl+KgpG5Xt6wN5rFTLjRf/Uf3pfg=
|
github.com/go-acme/lego/v4 v4.3.1 h1:rzmg0Gpy25B/exXjl+KgpG5Xt6wN5rFTLjRf/Uf3pfg=
|
||||||
github.com/go-acme/lego/v4 v4.3.1/go.mod h1:tySA24ifl6bI7kZ0+ocGtTIv4H1yhYVFAgyMHF2DSRg=
|
github.com/go-acme/lego/v4 v4.3.1/go.mod h1:tySA24ifl6bI7kZ0+ocGtTIv4H1yhYVFAgyMHF2DSRg=
|
||||||
@@ -124,21 +147,8 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
|
|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-i2p/checki2cp v0.0.0-20250819201001-7a3f89fafac8 h1:gOYWzWZKSSSeO6VendtDyEuTvR4WKxD5NLIxknDfLB8=
|
github.com/go-i2p/go-i2p v0.0.0-20250130205134-f144c457ba5d h1:D3Ah0QjtBNY/ADd6795pomzDi0fAYl7VpudwNw3kjAQ=
|
||||||
github.com/go-i2p/checki2cp v0.0.0-20250819201001-7a3f89fafac8/go.mod h1:h2Ufc73Qvj+KTkOz6H+JSS4XA7fM/Smqp593daAQNOc=
|
github.com/go-i2p/go-i2p v0.0.0-20250130205134-f144c457ba5d/go.mod h1:EU/fbQiZWSeBfStBTdijfuPMGSQLYuQNSE+ngT9vtiY=
|
||||||
github.com/go-i2p/common v0.0.0-20250819190749-01946d9f7ccf h1:rWDND6k+wt1jo96H8oZEphSu9Ig9UPGodR94azDRfxo=
|
|
||||||
github.com/go-i2p/common v0.0.0-20250819190749-01946d9f7ccf/go.mod h1:GD6iti2YU9LPrcESZ6Ty3lgxKGO7324tPhuKfYsJxrQ=
|
|
||||||
github.com/go-i2p/crypto v0.0.0-20250715200104-0ce55885b9cf h1:R7SX3WbuYX2YH9wCzNup2GY6efLN0j8BRbyeskDYWn8=
|
|
||||||
github.com/go-i2p/crypto v0.0.0-20250715200104-0ce55885b9cf/go.mod h1:1Y3NCpVg6OgE3c2VPRQ3QDmWPtDpJYLIyRBA1iJCd3E=
|
|
||||||
github.com/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4/go.mod h1:m5TlHjPZrU5KbTd7Lr+I2rljyC6aJ88HdkeMQXV0U0E=
|
|
||||||
github.com/go-i2p/i2pkeys v0.33.92 h1:e2vx3vf7tNesaJ8HmAlGPOcfiGM86jzeIGxh27I9J2Y=
|
|
||||||
github.com/go-i2p/i2pkeys v0.33.92/go.mod h1:BRURQ/twxV0WKjZlFSKki93ivBi+MirZPWudfwTzMpE=
|
|
||||||
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c h1:VTiECn3dFEmUlZjto+wOwJ7SSJTHPLyNprQMR5HzIMI=
|
|
||||||
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c/go.mod h1:te7Zj3g3oMeIl8uBXAgO62UKmZ6m6kHRNg1Mm+X8Hzk=
|
|
||||||
github.com/go-i2p/onramp v0.33.92 h1:Dk3A0SGpdEw829rSjW2LqN8o16pUvuhiN0vn36z7Gpc=
|
|
||||||
github.com/go-i2p/onramp v0.33.92/go.mod h1:5sfB8H2xk05gAS2K7XAUZ7ekOfwGJu3tWF0fqdXzJG4=
|
|
||||||
github.com/go-i2p/sam3 v0.33.92 h1:TVpi4GH7Yc7nZBiE1QxLjcZfnC4fI/80zxQz1Rk36BA=
|
|
||||||
github.com/go-i2p/sam3 v0.33.92/go.mod h1:oDuV145l5XWKKafeE4igJHTDpPwA0Yloz9nyKKh92eo=
|
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
@@ -181,9 +191,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
|
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -194,6 +203,7 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/renameio v1.0.0/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
@@ -264,6 +274,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
|
|||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
|
||||||
@@ -320,13 +332,12 @@ github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgP
|
|||||||
github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
|
github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
|
||||||
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
|
||||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
|
||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||||
@@ -335,9 +346,9 @@ github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc
|
|||||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||||
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||||
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
@@ -346,9 +357,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
|
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
@@ -375,8 +385,10 @@ github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKc
|
|||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/rglonek/untar v0.0.1 h1:fI1QmP07eQvOgudrUP/NDUCob56JuAYlLDknxX8485A=
|
github.com/rglonek/untar v0.0.1 h1:fI1QmP07eQvOgudrUP/NDUCob56JuAYlLDknxX8485A=
|
||||||
github.com/rglonek/untar v0.0.1/go.mod h1:yq/FZcge2BBdmPQEShskttgtHZG+LOtiHZyXknL54a0=
|
github.com/rglonek/untar v0.0.1/go.mod h1:yq/FZcge2BBdmPQEShskttgtHZG+LOtiHZyXknL54a0=
|
||||||
|
github.com/riobard/go-x25519 v0.0.0-20190716001027-10cc4d8d0b33/go.mod h1:BjmVxzAnkLeoEbqHEerI4eSw6ua+RaIB0S4jMV21RAs=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
|
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
|
||||||
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@@ -384,10 +396,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
|
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
|
||||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
|
||||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
|
||||||
github.com/samber/oops v1.19.0 h1:sfZAwC8MmTXBRRyNc4Z1utuTPBx+hFKF5fJ9DEQRZfw=
|
|
||||||
github.com/samber/oops v1.19.0/go.mod h1:+f+61dbiMxEMQ8gw/zTxW2pk+YGobaDM4glEHQtPOww=
|
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
@@ -415,20 +423,25 @@ github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/throttled/throttled/v2 v2.7.1 h1:FnBysDX4Sok55bvfDMI0l2Y71V1vM2wi7O79OW7fNtw=
|
github.com/throttled/throttled/v2 v2.7.1 h1:FnBysDX4Sok55bvfDMI0l2Y71V1vM2wi7O79OW7fNtw=
|
||||||
github.com/throttled/throttled/v2 v2.7.1/go.mod h1:fuOeyK9fmnA+LQnsBbfT/mmPHjmkdogRBQxaD8YsgZ8=
|
github.com/throttled/throttled/v2 v2.7.1/go.mod h1:fuOeyK9fmnA+LQnsBbfT/mmPHjmkdogRBQxaD8YsgZ8=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/transip/gotransip/v6 v6.6.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
|
github.com/transip/gotransip/v6 v6.6.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
|
||||||
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||||
|
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
|
||||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha h1:Cbc2CVsHVveE6SvoyOetqQKYNhxKsgp3bTlqH1nyi1Q=
|
github.com/urfave/cli/v3 v3.0.0-alpha h1:Cbc2CVsHVveE6SvoyOetqQKYNhxKsgp3bTlqH1nyi1Q=
|
||||||
@@ -443,6 +456,9 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
|||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
|
github.com/ybbus/jsonrpc/v2 v2.1.7/go.mod h1:rIuG1+ORoiqocf9xs/v+ecaAVeo3zcZHQgInyKFMeg0=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA=
|
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA=
|
||||||
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow=
|
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow=
|
||||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g=
|
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g=
|
||||||
@@ -462,12 +478,6 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
|||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
|
||||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
|
||||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
|
||||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
|
||||||
go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU=
|
|
||||||
go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU=
|
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
@@ -487,9 +497,13 @@ golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -519,6 +533,8 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
|||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -551,8 +567,13 @@ golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -564,8 +585,10 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -611,12 +634,26 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -624,8 +661,14 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -666,9 +709,13 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK
|
|||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
@@ -722,6 +769,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||||
@@ -755,6 +803,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
302
index.html
Normal file
302
index.html
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
I2P Reseed Tools
|
||||||
|
</title>
|
||||||
|
<meta name="author" content="eyedeekay" />
|
||||||
|
<meta name="description" content="reseed-tools" />
|
||||||
|
<meta name="keywords" content="master" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="showhider.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="navbar">
|
||||||
|
<a href="#shownav">
|
||||||
|
Show navigation
|
||||||
|
</a>
|
||||||
|
<div id="shownav">
|
||||||
|
<div id="hidenav">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="..">
|
||||||
|
Up one level ^
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="index.html">
|
||||||
|
index
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="CHANGELOG.html">
|
||||||
|
CHANGELOG
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="content/index.html">
|
||||||
|
content/index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/index.html">
|
||||||
|
docs/index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="index.html">
|
||||||
|
index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/DEBIAN.html">
|
||||||
|
docs/DEBIAN
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/DOCKER.html">
|
||||||
|
docs/DOCKER
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/EXAMPLES.html">
|
||||||
|
docs/EXAMPLES
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/PLUGIN.html">
|
||||||
|
docs/PLUGIN
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/index.html">
|
||||||
|
docs/index
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/SERVICES.html">
|
||||||
|
docs/SERVICES
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/TLS.html">
|
||||||
|
docs/TLS
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="docs/index.html">
|
||||||
|
docs/index.html
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
<a href="#hidenav">
|
||||||
|
Hide Navigation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a id="returnhome" href="/">
|
||||||
|
/
|
||||||
|
</a>
|
||||||
|
<h1>
|
||||||
|
I2P Reseed Tools
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
<img src="content/images/reseed.png" alt="Reseed Tools Poster" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This tool provides a secure and efficient reseed server for the I2P network.
|
||||||
|
There are several utility commands to create, sign, and validate SU3 files.
|
||||||
|
Please note that this requires at least Go version 1.13, and uses Go Modules.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Standard reseeds are distributed with the I2P packages. To get your reseed
|
||||||
|
included, apply on
|
||||||
|
<a href="http://zzz.i2p">
|
||||||
|
zzz.i2p
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<h2>
|
||||||
|
Dependencies
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<code>
|
||||||
|
go
|
||||||
|
</code>
|
||||||
|
,
|
||||||
|
<code>
|
||||||
|
git
|
||||||
|
</code>
|
||||||
|
, and optionally
|
||||||
|
<code>
|
||||||
|
make
|
||||||
|
</code>
|
||||||
|
are required to build the project.
|
||||||
|
Precompiled binaries for most platforms are available at my github mirror
|
||||||
|
<a href="https://github.com/eyedeekay/i2p-tools-1">
|
||||||
|
https://github.com/eyedeekay/i2p-tools-1
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In order to install the build-dependencies on Ubuntu or Debian, you may use:
|
||||||
|
</p>
|
||||||
|
<pre><code class="language-sh">sudo apt-get install golang-go git make
|
||||||
|
</code></pre>
|
||||||
|
<h2>
|
||||||
|
Installation
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Reseed-tools can be run as a user, as a freestanding service, or be installed
|
||||||
|
as an I2P Plugin. It will attempt to configure itself automatically. You should
|
||||||
|
make sure to set the
|
||||||
|
<code>
|
||||||
|
--signer
|
||||||
|
</code>
|
||||||
|
flag or the
|
||||||
|
<code>
|
||||||
|
RESEED_EMAIL
|
||||||
|
</code>
|
||||||
|
environment variable
|
||||||
|
to configure your signing keys/contact info.
|
||||||
|
</p>
|
||||||
|
<h3>
|
||||||
|
Installation(From Source)
|
||||||
|
</h3>
|
||||||
|
<pre><code>git clone https://i2pgit.org/idk/reseed-tools
|
||||||
|
cd reseed-tools
|
||||||
|
make build
|
||||||
|
# Optionally, if you want to install to /usr/bin/reseed-tools
|
||||||
|
sudo make install
|
||||||
|
</code></pre>
|
||||||
|
<h2>
|
||||||
|
Usage
|
||||||
|
</h2>
|
||||||
|
<h4>
|
||||||
|
Debian/Ubuntu note:
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
It is possible to create a
|
||||||
|
<code>
|
||||||
|
.deb
|
||||||
|
</code>
|
||||||
|
package using
|
||||||
|
<a href="docs/DEBIAN.md">
|
||||||
|
these instructions
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Debian users who are running I2P as a system service must also run the
|
||||||
|
<code>
|
||||||
|
reseed-tools
|
||||||
|
</code>
|
||||||
|
as the same user. This is so that the reseed-tools can access
|
||||||
|
the I2P service’s netDb directory. On Debian and Ubuntu, that user is
|
||||||
|
<code>
|
||||||
|
i2psvc
|
||||||
|
</code>
|
||||||
|
and the netDb directory is:
|
||||||
|
<code>
|
||||||
|
/var/lib/i2p/i2p-config/netDb
|
||||||
|
</code>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<h2>
|
||||||
|
Example Commands:
|
||||||
|
</h2>
|
||||||
|
<h3>
|
||||||
|
Without a webserver, standalone with TLS support
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
If this is your first time running a reseed server (ie. you don’t have any existing keys),
|
||||||
|
you can simply run the command and follow the prompts to create the appropriate keys, crl and certificates.
|
||||||
|
Afterwards an HTTPS reseed server will start on the default port and generate 6 files in your current directory
|
||||||
|
(a TLS key, certificate and crl, and a su3-file signing key, certificate and crl).
|
||||||
|
</p>
|
||||||
|
<pre><code>reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --tlsHost=your-domain.tld
|
||||||
|
</code></pre>
|
||||||
|
<h3>
|
||||||
|
Locally behind a webserver (reverse proxy setup), preferred:
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
If you are using a reverse proxy server it may provide the TLS certificate instead.
|
||||||
|
</p>
|
||||||
|
<pre><code>reseed-tools reseed --signer=you@mail.i2p --netdb=/home/i2p/.i2p/netDb --port=8443 --ip=127.0.0.1 --trustProxy
|
||||||
|
</code></pre>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
Usage
|
||||||
|
</strong>
|
||||||
|
<a href="docs/EXAMPLES.md">
|
||||||
|
More examples can be found here.
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
Docker
|
||||||
|
</strong>
|
||||||
|
<a href="docs/DOCKER.md">
|
||||||
|
Docker examples can be found here
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div id="sourcecode">
|
||||||
|
<span id="sourcehead">
|
||||||
|
<strong>
|
||||||
|
Get the source code:
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://i2pgit.org/idk/reseed-tools">
|
||||||
|
Source Repository: (https://i2pgit.org/idk/reseed-tools)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="#show">
|
||||||
|
Show license
|
||||||
|
</a>
|
||||||
|
<div id="show">
|
||||||
|
<div id="hide">
|
||||||
|
<pre><code>Copyright (c) 2014 Matt Drollette
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
</code></pre>
|
||||||
|
<a href="#hide">
|
||||||
|
Hide license
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<iframe src="https://snowflake.torproject.org/embed.html" width="320" height="240" frameborder="0" scrolling="no"></iframe>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://geti2p.net/">
|
||||||
|
<img src="i2plogo.png"></img>
|
||||||
|
I2P
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
main.go
16
main.go
@@ -4,15 +4,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/go-i2p/logger"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"i2pgit.org/go-i2p/reseed-tools/cmd"
|
"i2pgit.org/idk/reseed-tools/cmd"
|
||||||
"i2pgit.org/go-i2p/reseed-tools/reseed"
|
"i2pgit.org/idk/reseed-tools/reseed"
|
||||||
)
|
)
|
||||||
|
|
||||||
var lgr = logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// TLS 1.3 is available only on an opt-in basis in Go 1.12.
|
||||||
|
// To enable it, set the GODEBUG environment variable (comma-separated key=value options) such that it includes "tls13=1".
|
||||||
|
// To enable it from within the process, set the environment variable before any use of TLS:
|
||||||
|
os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1")
|
||||||
|
|
||||||
// use at most half the cpu cores
|
// use at most half the cpu cores
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU() / 2)
|
runtime.GOMAXPROCS(runtime.NumCPU() / 2)
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ func main() {
|
|||||||
app.Version = reseed.Version
|
app.Version = reseed.Version
|
||||||
app.Usage = "I2P tools and reseed server"
|
app.Usage = "I2P tools and reseed server"
|
||||||
auth := &cli.Author{
|
auth := &cli.Author{
|
||||||
Name: "go-i2p",
|
Name: "eyedeekay",
|
||||||
Email: "hankhill19580@gmail.com",
|
Email: "hankhill19580@gmail.com",
|
||||||
}
|
}
|
||||||
app.Authors = append(app.Authors, auth)
|
app.Authors = append(app.Authors, auth)
|
||||||
@@ -31,13 +33,11 @@ func main() {
|
|||||||
cmd.NewSu3VerifyCommand(),
|
cmd.NewSu3VerifyCommand(),
|
||||||
cmd.NewKeygenCommand(),
|
cmd.NewKeygenCommand(),
|
||||||
cmd.NewShareCommand(),
|
cmd.NewShareCommand(),
|
||||||
cmd.NewDiagnoseCommand(),
|
|
||||||
cmd.NewVersionCommand(),
|
cmd.NewVersionCommand(),
|
||||||
// cmd.NewSu3VerifyPublicCommand(),
|
// cmd.NewSu3VerifyPublicCommand(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := app.Run(os.Args); err != nil {
|
||||||
lgr.WithError(err).Error("Application execution failed")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,44 +1,28 @@
|
|||||||
package reseed
|
package reseed
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blacklist manages a thread-safe collection of blocked IP addresses for reseed service security.
|
|
||||||
// It provides functionality to block specific IPs, load blacklists from files, and filter incoming
|
|
||||||
// connections to prevent access from malicious or unwanted sources. All operations are protected
|
|
||||||
// by a read-write mutex to support concurrent access patterns typical in network servers.
|
|
||||||
type Blacklist struct {
|
type Blacklist struct {
|
||||||
// blacklist stores the blocked IP addresses as a map for O(1) lookup performance
|
|
||||||
blacklist map[string]bool
|
blacklist map[string]bool
|
||||||
// m provides thread-safe access to the blacklist map using read-write semantics
|
m sync.RWMutex
|
||||||
m sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBlacklist creates a new empty blacklist instance with initialized internal structures.
|
|
||||||
// Returns a ready-to-use Blacklist that can immediately accept IP blocking operations and
|
|
||||||
// concurrent access from multiple goroutines handling network connections.
|
|
||||||
func NewBlacklist() *Blacklist {
|
func NewBlacklist() *Blacklist {
|
||||||
return &Blacklist{blacklist: make(map[string]bool), m: sync.RWMutex{}}
|
return &Blacklist{blacklist: make(map[string]bool), m: sync.RWMutex{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFile reads IP addresses from a text file and adds them to the blacklist.
|
|
||||||
// Each line in the file should contain one IP address. Empty lines are ignored.
|
|
||||||
// Returns error if file cannot be read, otherwise successfully populates the blacklist.
|
|
||||||
func (s *Blacklist) LoadFile(file string) error {
|
func (s *Blacklist) LoadFile(file string) error {
|
||||||
// Skip processing if empty filename provided to avoid unnecessary file operations
|
|
||||||
if file != "" {
|
if file != "" {
|
||||||
if content, err := os.ReadFile(file); err == nil {
|
if content, err := ioutil.ReadFile(file); err == nil {
|
||||||
// Process each line as a separate IP address for blocking
|
|
||||||
for _, ip := range strings.Split(string(content), "\n") {
|
for _, ip := range strings.Split(string(content), "\n") {
|
||||||
s.BlockIP(ip)
|
s.BlockIP(ip)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lgr.WithError(err).WithField("blacklist_file", file).Error("Failed to load blacklist file")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,11 +30,7 @@ func (s *Blacklist) LoadFile(file string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockIP adds an IP address to the blacklist for connection filtering.
|
|
||||||
// The IP will be rejected in all future connection attempts until the blacklist is cleared.
|
|
||||||
// This method is thread-safe and can be called concurrently from multiple goroutines.
|
|
||||||
func (s *Blacklist) BlockIP(ip string) {
|
func (s *Blacklist) BlockIP(ip string) {
|
||||||
// Acquire write lock to safely modify the blacklist map
|
|
||||||
s.m.Lock()
|
s.m.Lock()
|
||||||
defer s.m.Unlock()
|
defer s.m.Unlock()
|
||||||
|
|
||||||
@@ -58,7 +38,6 @@ func (s *Blacklist) BlockIP(ip string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Blacklist) isBlocked(ip string) bool {
|
func (s *Blacklist) isBlocked(ip string) bool {
|
||||||
// Use read lock for concurrent access during connection checking
|
|
||||||
s.m.RLock()
|
s.m.RLock()
|
||||||
defer s.m.RUnlock()
|
defer s.m.RUnlock()
|
||||||
|
|
||||||
@@ -73,26 +52,20 @@ type blacklistListener struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ln blacklistListener) Accept() (net.Conn, error) {
|
func (ln blacklistListener) Accept() (net.Conn, error) {
|
||||||
// Accept incoming TCP connection for blacklist evaluation
|
|
||||||
tc, err := ln.AcceptTCP()
|
tc, err := ln.AcceptTCP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).Error("Failed to accept TCP connection")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract IP address from remote connection for blacklist checking
|
|
||||||
ip, _, err := net.SplitHostPort(tc.RemoteAddr().String())
|
ip, _, err := net.SplitHostPort(tc.RemoteAddr().String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).WithField("remote_addr", tc.RemoteAddr().String()).Error("Failed to parse remote address")
|
|
||||||
tc.Close()
|
tc.Close()
|
||||||
return tc, err
|
return tc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject connection immediately if IP is blacklisted for security
|
|
||||||
if ln.blacklist.isBlocked(ip) {
|
if ln.blacklist.isBlocked(ip) {
|
||||||
lgr.WithField("blocked_ip", ip).Warn("Connection rejected: IP address is blacklisted")
|
|
||||||
tc.Close()
|
tc.Close()
|
||||||
return nil, errors.New("connection rejected: IP address is blacklisted")
|
return tc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return tc, err
|
return tc, err
|
||||||
|
@@ -1,412 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewBlacklist(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
|
|
||||||
if bl == nil {
|
|
||||||
t.Fatal("NewBlacklist() returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if bl.blacklist == nil {
|
|
||||||
t.Error("blacklist map not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(bl.blacklist) != 0 {
|
|
||||||
t.Error("blacklist should be empty initially")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_BlockIP(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ip string
|
|
||||||
}{
|
|
||||||
{"Valid IPv4", "192.168.1.1"},
|
|
||||||
{"Valid IPv6", "2001:db8::1"},
|
|
||||||
{"Localhost", "127.0.0.1"},
|
|
||||||
{"Empty string", ""},
|
|
||||||
{"Invalid IP format", "not.an.ip"},
|
|
||||||
{"IP with port", "192.168.1.1:8080"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
bl.BlockIP(tt.ip)
|
|
||||||
|
|
||||||
// Check if IP was added to blacklist
|
|
||||||
bl.m.RLock()
|
|
||||||
blocked, exists := bl.blacklist[tt.ip]
|
|
||||||
bl.m.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
t.Errorf("IP %s was not added to blacklist", tt.ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !blocked {
|
|
||||||
t.Errorf("IP %s should be marked as blocked", tt.ip)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_BlockIP_Concurrent(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Test concurrent access to BlockIP
|
|
||||||
ips := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4", "192.168.1.5"}
|
|
||||||
|
|
||||||
for _, ip := range ips {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(testIP string) {
|
|
||||||
defer wg.Done()
|
|
||||||
bl.BlockIP(testIP)
|
|
||||||
}(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Verify all IPs were blocked
|
|
||||||
for _, ip := range ips {
|
|
||||||
if !bl.isBlocked(ip) {
|
|
||||||
t.Errorf("IP %s should be blocked after concurrent operations", ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_isBlocked(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
|
|
||||||
// Test with non-blocked IP
|
|
||||||
if bl.isBlocked("192.168.1.1") {
|
|
||||||
t.Error("IP should not be blocked initially")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block an IP and test
|
|
||||||
bl.BlockIP("192.168.1.1")
|
|
||||||
if !bl.isBlocked("192.168.1.1") {
|
|
||||||
t.Error("IP should be blocked after calling BlockIP")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with different IP
|
|
||||||
if bl.isBlocked("192.168.1.2") {
|
|
||||||
t.Error("Different IP should not be blocked")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_isBlocked_Concurrent(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
bl.BlockIP("192.168.1.1")
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
results := make([]bool, 10)
|
|
||||||
|
|
||||||
// Test concurrent reads
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(index int) {
|
|
||||||
defer wg.Done()
|
|
||||||
results[index] = bl.isBlocked("192.168.1.1")
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// All reads should return true
|
|
||||||
for i, result := range results {
|
|
||||||
if !result {
|
|
||||||
t.Errorf("Concurrent read %d should return true for blocked IP", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_LoadFile_Success(t *testing.T) {
|
|
||||||
// Create temporary file with IP addresses
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
tempFile := filepath.Join(tempDir, "blacklist.txt")
|
|
||||||
|
|
||||||
ipList := "192.168.1.1\n192.168.1.2\n10.0.0.1\n127.0.0.1"
|
|
||||||
err := os.WriteFile(tempFile, []byte(ipList), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bl := NewBlacklist()
|
|
||||||
err = bl.LoadFile(tempFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LoadFile() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that all IPs from file are blocked
|
|
||||||
expectedIPs := strings.Split(ipList, "\n")
|
|
||||||
for _, ip := range expectedIPs {
|
|
||||||
if !bl.isBlocked(ip) {
|
|
||||||
t.Errorf("IP %s from file should be blocked", ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_LoadFile_EmptyFile(t *testing.T) {
|
|
||||||
// Create empty temporary file
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
tempFile := filepath.Join(tempDir, "empty_blacklist.txt")
|
|
||||||
|
|
||||||
err := os.WriteFile(tempFile, []byte(""), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bl := NewBlacklist()
|
|
||||||
err = bl.LoadFile(tempFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LoadFile() should not fail with empty file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have one entry (empty string)
|
|
||||||
if !bl.isBlocked("") {
|
|
||||||
t.Error("Empty string should be blocked when loading empty file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_LoadFile_FileNotFound(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
err := bl.LoadFile("/nonexistent/file.txt")
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Error("LoadFile() should return error for non-existent file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_LoadFile_EmptyString(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
err := bl.LoadFile("")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("LoadFile() should not fail with empty filename: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should not block anything when no file is provided
|
|
||||||
if bl.isBlocked("192.168.1.1") {
|
|
||||||
t.Error("No IPs should be blocked when empty filename provided")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_LoadFile_WithWhitespace(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
tempFile := filepath.Join(tempDir, "whitespace_blacklist.txt")
|
|
||||||
|
|
||||||
// File with various whitespace scenarios
|
|
||||||
ipList := "192.168.1.1\n\n192.168.1.2\n \n10.0.0.1\n"
|
|
||||||
err := os.WriteFile(tempFile, []byte(ipList), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bl := NewBlacklist()
|
|
||||||
err = bl.LoadFile(tempFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LoadFile() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test specific IPs
|
|
||||||
if !bl.isBlocked("192.168.1.1") {
|
|
||||||
t.Error("IP 192.168.1.1 should be blocked")
|
|
||||||
}
|
|
||||||
if !bl.isBlocked("192.168.1.2") {
|
|
||||||
t.Error("IP 192.168.1.2 should be blocked")
|
|
||||||
}
|
|
||||||
if !bl.isBlocked("10.0.0.1") {
|
|
||||||
t.Error("IP 10.0.0.1 should be blocked")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty lines should also be "blocked" as they are processed as strings
|
|
||||||
if !bl.isBlocked("") {
|
|
||||||
t.Error("Empty string should be blocked due to empty lines")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBlacklistListener(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
|
|
||||||
// Create a test TCP listener
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test listener: %v", err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
|
|
||||||
blListener := newBlacklistListener(listener, bl)
|
|
||||||
|
|
||||||
if blListener.blacklist != bl {
|
|
||||||
t.Error("blacklist reference not set correctly")
|
|
||||||
}
|
|
||||||
|
|
||||||
if blListener.TCPListener == nil {
|
|
||||||
t.Error("TCPListener not set correctly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklistListener_Accept_AllowedConnection(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
|
|
||||||
// Create a test TCP listener
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test listener: %v", err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
|
|
||||||
blListener := newBlacklistListener(listener, bl)
|
|
||||||
|
|
||||||
// Create a connection in a goroutine
|
|
||||||
go func() {
|
|
||||||
time.Sleep(10 * time.Millisecond) // Small delay to ensure Accept is called first
|
|
||||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
|
||||||
if err == nil {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
conn, err := blListener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Accept() failed for allowed connection: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn == nil {
|
|
||||||
t.Error("Connection should not be nil for allowed IP")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn != nil {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklistListener_Accept_BlockedConnection(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
bl.BlockIP("127.0.0.1")
|
|
||||||
|
|
||||||
// Create a test TCP listener
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test listener: %v", err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
|
|
||||||
blListener := newBlacklistListener(listener, bl)
|
|
||||||
|
|
||||||
// Create a connection in a goroutine
|
|
||||||
go func() {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
|
||||||
if err == nil {
|
|
||||||
// Connection might be closed immediately, but that's expected
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
conn, err := blListener.Accept()
|
|
||||||
// For blocked connections, Accept should return an error
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Accept() should return an error for blocked connections")
|
|
||||||
if conn != nil {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn != nil {
|
|
||||||
t.Error("Accept() should return nil connection for blocked IPs")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the error message is appropriate
|
|
||||||
if err != nil && !strings.Contains(err.Error(), "blacklisted") {
|
|
||||||
t.Errorf("Expected error message to contain 'blacklisted', got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklistListener_Accept_ErrorBehavior(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
bl.BlockIP("127.0.0.1")
|
|
||||||
|
|
||||||
// Create a test TCP listener
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test listener: %v", err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
|
|
||||||
blListener := newBlacklistListener(listener, bl)
|
|
||||||
|
|
||||||
// Create a connection from the blacklisted IP
|
|
||||||
go func() {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
|
||||||
if err == nil {
|
|
||||||
defer conn.Close()
|
|
||||||
// Try to write some data to ensure connection is established
|
|
||||||
conn.Write([]byte("test"))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
conn, err := blListener.Accept()
|
|
||||||
|
|
||||||
// Verify the error behavior
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected error for blacklisted IP, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn != nil {
|
|
||||||
t.Error("Expected nil connection for blacklisted IP, got non-nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedErrMsg := "connection rejected: IP address is blacklisted"
|
|
||||||
if err.Error() != expectedErrMsg {
|
|
||||||
t.Errorf("Expected error message '%s', got '%s'", expectedErrMsg, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlacklist_ThreadSafety(t *testing.T) {
|
|
||||||
bl := NewBlacklist()
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Test concurrent operations
|
|
||||||
numGoroutines := 10
|
|
||||||
numOperations := 100
|
|
||||||
|
|
||||||
// Concurrent BlockIP operations
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < numOperations; j++ {
|
|
||||||
ip := "192.168." + string(rune(id)) + "." + string(rune(j))
|
|
||||||
bl.BlockIP(ip)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Concurrent isBlocked operations
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < numOperations; j++ {
|
|
||||||
ip := "10.0." + string(rune(id)) + "." + string(rune(j))
|
|
||||||
bl.isBlocked(ip) // Result doesn't matter, just testing for races
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// If we get here without data races, the test passes
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
// Version defines the current release version of the reseed-tools application.
|
|
||||||
// This version string is used for compatibility checking, update notifications,
|
|
||||||
// and identifying the software version in server responses and logs.
|
|
||||||
const Version = "0.3.10"
|
|
||||||
|
|
||||||
// HTTP User-Agent constants for I2P protocol compatibility
|
|
||||||
const (
|
|
||||||
// I2pUserAgent mimics wget for I2P router compatibility and standardized request handling.
|
|
||||||
// Many I2P implementations expect this specific user agent string for proper reseed operations.
|
|
||||||
I2pUserAgent = "Wget/1.11.4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Random string generation constants for secure token creation
|
|
||||||
const (
|
|
||||||
// letterBytes contains all valid characters for generating random alphabetic strings
|
|
||||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 52 possibilities
|
|
||||||
// letterIdxBits specifies the number of bits needed to represent character indices
|
|
||||||
letterIdxBits = 6 // 6 bits to represent 64 possibilities / indexes
|
|
||||||
// letterIdxMask provides bit masking for efficient random character selection
|
|
||||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
|
||||||
)
|
|
@@ -2,6 +2,9 @@ package reseed
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
_ "embed"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,16 +15,9 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// f contains the embedded static content files for the reseed server web interface.
|
|
||||||
// This includes HTML templates, CSS stylesheets, JavaScript files, and localized content
|
|
||||||
// for serving the homepage and user interface to reseed service clients.
|
|
||||||
//
|
|
||||||
//go:embed content
|
//go:embed content
|
||||||
var f embed.FS
|
var f embed.FS
|
||||||
|
|
||||||
// SupportedLanguages defines all languages available for the reseed server homepage.
|
|
||||||
// These language tags are used for content localization and browser language matching
|
|
||||||
// to provide multilingual support for users accessing the reseed service web interface.
|
|
||||||
var SupportedLanguages = []language.Tag{
|
var SupportedLanguages = []language.Tag{
|
||||||
language.English,
|
language.English,
|
||||||
language.Russian,
|
language.Russian,
|
||||||
@@ -39,23 +35,12 @@ var SupportedLanguages = []language.Tag{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// CachedLanguagePages stores pre-processed language-specific content pages for performance.
|
|
||||||
// Keys are language directory paths and values are rendered HTML content to avoid
|
|
||||||
// repeated markdown processing on each request for better response times.
|
|
||||||
CachedLanguagePages = map[string]string{}
|
CachedLanguagePages = map[string]string{}
|
||||||
// CachedDataPages stores static file content in memory for faster serving.
|
CachedDataPages = map[string][]byte{}
|
||||||
// Keys are file paths and values are raw file content bytes to reduce filesystem I/O
|
|
||||||
// and improve performance for frequently accessed static resources.
|
|
||||||
CachedDataPages = map[string][]byte{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StableContentPath returns the path to static content files for the reseed server homepage.
|
|
||||||
// It automatically extracts embedded content to the filesystem if not already present and
|
|
||||||
// ensures the content directory structure is available for serving web requests.
|
|
||||||
func StableContentPath() (string, error) {
|
func StableContentPath() (string, error) {
|
||||||
// Attempt to get the base content path from the system
|
|
||||||
BaseContentPath, ContentPathError := ContentPath()
|
BaseContentPath, ContentPathError := ContentPath()
|
||||||
// Extract embedded content if directory doesn't exist
|
|
||||||
if _, err := os.Stat(BaseContentPath); os.IsNotExist(err) {
|
if _, err := os.Stat(BaseContentPath); os.IsNotExist(err) {
|
||||||
if err := unembed.Unembed(f, BaseContentPath); err != nil {
|
if err := unembed.Unembed(f, BaseContentPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -66,14 +51,8 @@ func StableContentPath() (string, error) {
|
|||||||
return BaseContentPath, ContentPathError
|
return BaseContentPath, ContentPathError
|
||||||
}
|
}
|
||||||
|
|
||||||
// matcher provides language matching functionality for reseed server internationalization.
|
|
||||||
// It uses the SupportedLanguages list to match client browser language preferences
|
|
||||||
// with available localized content for optimal user experience.
|
|
||||||
var matcher = language.NewMatcher(SupportedLanguages)
|
var matcher = language.NewMatcher(SupportedLanguages)
|
||||||
|
|
||||||
// header contains the standard HTML document header for reseed server web pages.
|
|
||||||
// This template includes essential meta tags, CSS stylesheet links, and JavaScript
|
|
||||||
// imports needed for consistent styling and functionality across all served pages.
|
|
||||||
var header = []byte(`<!DOCTYPE html>
|
var header = []byte(`<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -84,20 +63,11 @@ var header = []byte(`<!DOCTYPE html>
|
|||||||
</head>
|
</head>
|
||||||
<body>`)
|
<body>`)
|
||||||
|
|
||||||
// footer contains the closing HTML tags for reseed server web pages.
|
|
||||||
// This template ensures proper document structure termination for all served content
|
|
||||||
// and maintains valid HTML5 compliance across the web interface.
|
|
||||||
var footer = []byte(` </body>
|
var footer = []byte(` </body>
|
||||||
</html>`)
|
</html>`)
|
||||||
|
|
||||||
// md provides configured markdown processor for reseed server content rendering.
|
|
||||||
// It supports XHTML output and embedded HTML for converting markdown files to
|
|
||||||
// properly formatted web content with security and standards compliance.
|
|
||||||
var md = markdown.New(markdown.XHTMLOutput(true), markdown.HTML(true))
|
var md = markdown.New(markdown.XHTMLOutput(true), markdown.HTML(true))
|
||||||
|
|
||||||
// ContentPath determines the filesystem path where reseed server content should be stored.
|
|
||||||
// It checks the current working directory and creates a content subdirectory for serving
|
|
||||||
// static files like HTML, CSS, and localized content to reseed service users.
|
|
||||||
func ContentPath() (string, error) {
|
func ContentPath() (string, error) {
|
||||||
exPath, err := os.Getwd()
|
exPath, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -110,147 +80,69 @@ func ContentPath() (string, error) {
|
|||||||
return filepath.Join(exPath, "content"), nil
|
return filepath.Join(exPath, "content"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleARealBrowser processes HTTP requests from web browsers and serves appropriate content.
|
|
||||||
// This function routes browser requests to the correct content handlers based on URL path
|
|
||||||
// and provides language localization support for the reseed server's web interface.
|
|
||||||
func (srv *Server) HandleARealBrowser(w http.ResponseWriter, r *http.Request) {
|
func (srv *Server) HandleARealBrowser(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := srv.validateContentPath(); err != nil {
|
_, ContentPathError := StableContentPath()
|
||||||
|
if ContentPathError != nil {
|
||||||
http.Error(w, "403 Forbidden", http.StatusForbidden)
|
http.Error(w, "403 Forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine client's preferred language from headers and cookies
|
|
||||||
baseLanguage := srv.determineClientLanguage(r)
|
|
||||||
|
|
||||||
// Route request to appropriate handler based on URL path
|
|
||||||
srv.routeRequest(w, r, baseLanguage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateContentPath ensures the content directory exists and is accessible.
|
|
||||||
// Returns an error if content cannot be served.
|
|
||||||
func (srv *Server) validateContentPath() error {
|
|
||||||
_, ContentPathError := StableContentPath()
|
|
||||||
return ContentPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
// determineClientLanguage extracts and processes language preferences from the HTTP request.
|
|
||||||
// It uses both cookie values and Accept-Language headers to determine the best language match.
|
|
||||||
func (srv *Server) determineClientLanguage(r *http.Request) string {
|
|
||||||
lang, _ := r.Cookie("lang")
|
lang, _ := r.Cookie("lang")
|
||||||
accept := r.Header.Get("Accept-Language")
|
accept := r.Header.Get("Accept-Language")
|
||||||
|
log.Printf("lang: '%s', accept: '%s'\n", lang, accept)
|
||||||
lgr.WithField("lang", lang).WithField("accept", accept).Debug("Processing language preferences")
|
|
||||||
srv.logRequestHeaders(r)
|
|
||||||
|
|
||||||
tag, _ := language.MatchStrings(matcher, lang.String(), accept)
|
|
||||||
lgr.WithField("tag", tag).Debug("Matched language tag")
|
|
||||||
|
|
||||||
base, _ := tag.Base()
|
|
||||||
lgr.WithField("base", base).Debug("Base language")
|
|
||||||
|
|
||||||
return base.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// logRequestHeaders logs all HTTP request headers for debugging purposes.
|
|
||||||
func (srv *Server) logRequestHeaders(r *http.Request) {
|
|
||||||
for name, values := range r.Header {
|
for name, values := range r.Header {
|
||||||
|
// Loop over all values for the name.
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
lgr.WithField("header_name", name).WithField("header_value", value).Debug("Request header")
|
log.Printf("name: '%s', value: '%s'\n", name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag, _ := language.MatchStrings(matcher, lang.String(), accept)
|
||||||
|
log.Printf("tag: '%s'\n", tag)
|
||||||
|
base, _ := tag.Base()
|
||||||
|
log.Printf("base: '%s'\n", base)
|
||||||
|
|
||||||
|
if strings.HasSuffix(r.URL.Path, "style.css") {
|
||||||
|
w.Header().Set("Content-Type", "text/css")
|
||||||
|
HandleAFile(w, "", "style.css")
|
||||||
|
} else if strings.HasSuffix(r.URL.Path, "script.js") {
|
||||||
|
w.Header().Set("Content-Type", "text/javascript")
|
||||||
|
HandleAFile(w, "", "script.js")
|
||||||
|
} else {
|
||||||
|
image := strings.Replace(r.URL.Path, "/", "", -1)
|
||||||
|
if strings.HasPrefix(image, "images") {
|
||||||
|
w.Header().Set("Content-Type", "image/png")
|
||||||
|
HandleAFile(w, "images", strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, "/"), "images"))
|
||||||
|
} else if strings.HasPrefix(image, "ping") {
|
||||||
|
PingEverybody()
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
} else if strings.HasPrefix(image, "readout") {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Write([]byte(header))
|
||||||
|
ReadOut(w)
|
||||||
|
w.Write([]byte(footer))
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Write([]byte(header))
|
||||||
|
HandleALocalizedFile(w, base.String())
|
||||||
|
w.Write([]byte(`<ul><li><form method="post" action="/i2pseeds" class="inline">
|
||||||
|
<input type="hidden" name="onetime" value="` + srv.Acceptable() + `">
|
||||||
|
<button type="submit" name="submit_param" value="submit_value" class="link-button">
|
||||||
|
Reseed
|
||||||
|
</button>
|
||||||
|
</form></li></ul>`))
|
||||||
|
ReadOut(w)
|
||||||
|
w.Write([]byte(footer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// routeRequest dispatches HTTP requests to the appropriate content handler based on URL path.
|
func HandleAFile(w http.ResponseWriter, dirPath, file string) {
|
||||||
// Supports CSS files, JavaScript files, images, ping functionality, readout pages, and localized content.
|
|
||||||
func (srv *Server) routeRequest(w http.ResponseWriter, r *http.Request, baseLanguage string) {
|
|
||||||
if strings.HasSuffix(r.URL.Path, "style.css") {
|
|
||||||
srv.handleCSSRequest(w)
|
|
||||||
} else if strings.HasSuffix(r.URL.Path, "script.js") {
|
|
||||||
srv.handleJavaScriptRequest(w)
|
|
||||||
} else {
|
|
||||||
srv.handleDynamicRequest(w, r, baseLanguage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCSSRequest serves CSS stylesheet files with appropriate content type headers.
|
|
||||||
func (srv *Server) handleCSSRequest(w http.ResponseWriter) {
|
|
||||||
w.Header().Set("Content-Type", "text/css")
|
|
||||||
handleAFile(w, "", "style.css")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleJavaScriptRequest serves JavaScript files with appropriate content type headers.
|
|
||||||
func (srv *Server) handleJavaScriptRequest(w http.ResponseWriter) {
|
|
||||||
w.Header().Set("Content-Type", "text/javascript")
|
|
||||||
handleAFile(w, "", "script.js")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleDynamicRequest processes requests for images, special functions, and localized content.
|
|
||||||
// Routes to appropriate handlers for images, ping operations, readout pages, and main homepage.
|
|
||||||
func (srv *Server) handleDynamicRequest(w http.ResponseWriter, r *http.Request, baseLanguage string) {
|
|
||||||
image := strings.Replace(r.URL.Path, "/", "", -1)
|
|
||||||
|
|
||||||
if strings.HasPrefix(image, "images") {
|
|
||||||
srv.handleImageRequest(w, r)
|
|
||||||
} else if strings.HasPrefix(image, "ping") {
|
|
||||||
srv.handlePingRequest(w, r)
|
|
||||||
} else if strings.HasPrefix(image, "readout") {
|
|
||||||
srv.handleReadoutRequest(w)
|
|
||||||
} else {
|
|
||||||
srv.handleHomepageRequest(w, baseLanguage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleImageRequest serves image files with PNG content type headers.
|
|
||||||
func (srv *Server) handleImageRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "image/png")
|
|
||||||
imagePath := strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, "/"), "images")
|
|
||||||
handleAFile(w, "images", imagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlePingRequest processes ping functionality and redirects to homepage.
|
|
||||||
func (srv *Server) handlePingRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
PingEverybody()
|
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleReadoutRequest serves the readout page with status information.
|
|
||||||
func (srv *Server) handleReadoutRequest(w http.ResponseWriter) {
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.Write([]byte(header))
|
|
||||||
ReadOut(w)
|
|
||||||
w.Write([]byte(footer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleHomepageRequest serves the main homepage with localized content and reseed functionality.
|
|
||||||
func (srv *Server) handleHomepageRequest(w http.ResponseWriter, baseLanguage string) {
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.Write([]byte(header))
|
|
||||||
handleALocalizedFile(w, baseLanguage)
|
|
||||||
|
|
||||||
// Add reseed form with one-time token
|
|
||||||
reseedForm := `<ul><li><form method="post" action="/i2pseeds" class="inline">
|
|
||||||
<input type="hidden" name="onetime" value="` + srv.Acceptable() + `">
|
|
||||||
<button type="submit" name="submit_param" value="submit_value" class="link-button">
|
|
||||||
Reseed
|
|
||||||
</button>
|
|
||||||
</form></li></ul>`
|
|
||||||
w.Write([]byte(reseedForm))
|
|
||||||
|
|
||||||
ReadOut(w)
|
|
||||||
w.Write([]byte(footer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleAFile serves static files from the reseed server content directory with caching.
|
|
||||||
// It loads files from the filesystem on first access and caches them in memory for
|
|
||||||
// improved performance on subsequent requests, supporting CSS, JavaScript, and image files.
|
|
||||||
func handleAFile(w http.ResponseWriter, dirPath, file string) {
|
|
||||||
BaseContentPath, _ := StableContentPath()
|
BaseContentPath, _ := StableContentPath()
|
||||||
file = filepath.Join(dirPath, file)
|
file = filepath.Join(dirPath, file)
|
||||||
if _, prs := CachedDataPages[file]; !prs {
|
if _, prs := CachedDataPages[file]; !prs {
|
||||||
path := filepath.Join(BaseContentPath, file)
|
path := filepath.Join(BaseContentPath, file)
|
||||||
f, err := os.ReadFile(path)
|
f, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Write([]byte("Oops! Something went wrong handling your language. Please file a bug at https://i2pgit.org/go-i2p/reseed-tools\n\t" + err.Error()))
|
w.Write([]byte("Oops! Something went wrong handling your language. Please file a bug at https://i2pgit.org/idk/reseed-tools\n\t" + err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
CachedDataPages[file] = f
|
CachedDataPages[file] = f
|
||||||
@@ -260,16 +152,13 @@ func handleAFile(w http.ResponseWriter, dirPath, file string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleALocalizedFile processes and serves language-specific content with markdown rendering.
|
func HandleALocalizedFile(w http.ResponseWriter, dirPath string) {
|
||||||
// It reads markdown files from language subdirectories, converts them to HTML, and caches
|
|
||||||
// the results for efficient serving of multilingual reseed server interface content.
|
|
||||||
func handleALocalizedFile(w http.ResponseWriter, dirPath string) {
|
|
||||||
if _, prs := CachedLanguagePages[dirPath]; !prs {
|
if _, prs := CachedLanguagePages[dirPath]; !prs {
|
||||||
BaseContentPath, _ := StableContentPath()
|
BaseContentPath, _ := StableContentPath()
|
||||||
dir := filepath.Join(BaseContentPath, "lang", dirPath)
|
dir := filepath.Join(BaseContentPath, "lang", dirPath)
|
||||||
files, err := os.ReadDir(dir)
|
files, err := ioutil.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Write([]byte("Oops! Something went wrong handling your language. Please file a bug at https://i2pgit.org/go-i2p/reseed-tools\n\t" + err.Error()))
|
w.Write([]byte("Oops! Something went wrong handling your language. Please file a bug at https://i2pgit.org/idk/reseed-tools\n\t" + err.Error()))
|
||||||
}
|
}
|
||||||
var f []byte
|
var f []byte
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@@ -278,9 +167,9 @@ func handleALocalizedFile(w http.ResponseWriter, dirPath string) {
|
|||||||
}
|
}
|
||||||
trimmedName := strings.TrimSuffix(file.Name(), ".md")
|
trimmedName := strings.TrimSuffix(file.Name(), ".md")
|
||||||
path := filepath.Join(dir, file.Name())
|
path := filepath.Join(dir, file.Name())
|
||||||
b, err := os.ReadFile(path)
|
b, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Write([]byte("Oops! Something went wrong handling your language. Please file a bug at https://i2pgit.org/go-i2p/reseed-tools\n\t" + err.Error()))
|
w.Write([]byte("Oops! Something went wrong handling your language. Please file a bug at https://i2pgit.org/idk/reseed-tools\n\t" + err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f = append(f, []byte(`<div id="`+trimmedName+`">`)...)
|
f = append(f, []byte(`<div id="`+trimmedName+`">`)...)
|
||||||
|
@@ -1,55 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KeyStore manages certificate and key storage for the reseed service.
|
|
||||||
// Moved from: utils.go
|
|
||||||
type KeyStore struct {
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewKeyStore creates a new KeyStore instance with the specified path.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func NewKeyStore(path string) *KeyStore {
|
|
||||||
return &KeyStore{
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReseederCertificate loads a reseed certificate for the given signer.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func (ks *KeyStore) ReseederCertificate(signer []byte) (*x509.Certificate, error) {
|
|
||||||
return ks.reseederCertificate("reseed", signer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirReseederCertificate loads a reseed certificate from a specific directory.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func (ks *KeyStore) DirReseederCertificate(dir string, signer []byte) (*x509.Certificate, error) {
|
|
||||||
return ks.reseederCertificate(dir, signer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reseederCertificate is a helper method to load certificates from the keystore.
|
|
||||||
// Moved from: utils.go
|
|
||||||
func (ks *KeyStore) reseederCertificate(dir string, signer []byte) (*x509.Certificate, error) {
|
|
||||||
certFile := filepath.Base(SignerFilename(string(signer)))
|
|
||||||
certPath := filepath.Join(ks.Path, dir, certFile)
|
|
||||||
certString, err := os.ReadFile(certPath)
|
|
||||||
if nil != err {
|
|
||||||
lgr.WithError(err).WithField("cert_file", certPath).WithField("signer", string(signer)).Error("Failed to read reseed certificate file")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certPem, _ := pem.Decode(certString)
|
|
||||||
cert, err := x509.ParseCertificate(certPem.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
lgr.WithError(err).WithField("cert_file", certPath).WithField("signer", string(signer)).Error("Failed to parse reseed certificate")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
@@ -1,118 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/cretz/bine/tor"
|
|
||||||
"github.com/go-i2p/i2pkeys"
|
|
||||||
"github.com/go-i2p/logger"
|
|
||||||
"github.com/go-i2p/onramp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var lgr = logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
func (srv *Server) ListenAndServe() error {
|
|
||||||
addr := srv.Addr
|
|
||||||
if addr == "" {
|
|
||||||
addr = ":http"
|
|
||||||
}
|
|
||||||
ln, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return srv.Serve(newBlacklistListener(ln, srv.Blacklist))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error {
|
|
||||||
addr := srv.Addr
|
|
||||||
if addr == "" {
|
|
||||||
addr = ":https"
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.TLSConfig == nil {
|
|
||||||
srv.TLSConfig = &tls.Config{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.TLSConfig.NextProtos == nil {
|
|
||||||
srv.TLSConfig.NextProtos = []string{"http/1.1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
srv.TLSConfig.Certificates = make([]tls.Certificate, 1)
|
|
||||||
srv.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsListener := tls.NewListener(newBlacklistListener(ln, srv.Blacklist), srv.TLSConfig)
|
|
||||||
return srv.Serve(tlsListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) ListenAndServeOnionTLS(startConf *tor.StartConf, listenConf *tor.ListenConf, certFile, keyFile string) error {
|
|
||||||
lgr.WithField("service", "onionv3-https").Debug("Starting and registering OnionV3 HTTPS service, please wait a couple of minutes...")
|
|
||||||
var err error
|
|
||||||
srv.Onion, err = onramp.NewOnion("reseed")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
srv.OnionListener, err = srv.Onion.ListenTLS()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lgr.WithField("service", "onionv3-https").WithField("address", srv.OnionListener.Addr().String()+".onion").WithField("protocol", "https").Debug("Onionv3 server started")
|
|
||||||
|
|
||||||
return srv.Serve(srv.OnionListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) ListenAndServeOnion(startConf *tor.StartConf, listenConf *tor.ListenConf) error {
|
|
||||||
lgr.WithField("service", "onionv3-http").Debug("Starting and registering OnionV3 HTTP service, please wait a couple of minutes...")
|
|
||||||
var err error
|
|
||||||
srv.Onion, err = onramp.NewOnion("reseed")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
srv.OnionListener, err = srv.Onion.Listen()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lgr.WithField("service", "onionv3-http").WithField("address", srv.OnionListener.Addr().String()+".onion").WithField("protocol", "http").Debug("Onionv3 server started")
|
|
||||||
|
|
||||||
return srv.Serve(srv.OnionListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) ListenAndServeI2PTLS(samaddr string, I2PKeys i2pkeys.I2PKeys, certFile, keyFile string) error {
|
|
||||||
lgr.WithField("service", "i2p-https").WithField("sam_address", samaddr).Debug("Starting and registering I2P HTTPS service, please wait a couple of minutes...")
|
|
||||||
var err error
|
|
||||||
srv.Garlic, err = onramp.NewGarlic("reseed-tls", samaddr, onramp.OPT_WIDE)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
srv.I2PListener, err = srv.Garlic.ListenTLS()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lgr.WithField("service", "i2p-https").WithField("address", srv.I2PListener.Addr().(i2pkeys.I2PAddr).Base32()).WithField("protocol", "https").Debug("I2P server started")
|
|
||||||
return srv.Serve(srv.I2PListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) ListenAndServeI2P(samaddr string, I2PKeys i2pkeys.I2PKeys) error {
|
|
||||||
lgr.WithField("service", "i2p-http").WithField("sam_address", samaddr).Debug("Starting and registering I2P service, please wait a couple of minutes...")
|
|
||||||
var err error
|
|
||||||
srv.Garlic, err = onramp.NewGarlic("reseed", samaddr, onramp.OPT_WIDE)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
srv.I2PListener, err = srv.Garlic.Listen()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lgr.WithField("service", "i2p-http").WithField("address", srv.I2PListener.Addr().(i2pkeys.I2PAddr).Base32()+".b32.i2p").WithField("protocol", "http").Debug("I2P server started")
|
|
||||||
return srv.Serve(srv.I2PListener)
|
|
||||||
}
|
|
@@ -1,118 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/go-i2p/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestLoggerIntegration verifies that the logger is properly integrated
|
|
||||||
func TestLoggerIntegration(t *testing.T) {
|
|
||||||
// Test that logger instance is available
|
|
||||||
if lgr == nil {
|
|
||||||
t.Error("Logger instance lgr should not be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that logger responds to environment variables
|
|
||||||
originalDebug := os.Getenv("DEBUG_I2P")
|
|
||||||
originalWarnFail := os.Getenv("WARNFAIL_I2P")
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
os.Setenv("DEBUG_I2P", originalDebug)
|
|
||||||
os.Setenv("WARNFAIL_I2P", originalWarnFail)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Test debug logging
|
|
||||||
os.Setenv("DEBUG_I2P", "debug")
|
|
||||||
os.Setenv("WARNFAIL_I2P", "")
|
|
||||||
|
|
||||||
// Create a fresh logger instance to pick up env changes
|
|
||||||
testLgr := logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
// These should not panic and should be safe to call
|
|
||||||
testLgr.Debug("Test debug message")
|
|
||||||
testLgr.WithField("test", "value").Debug("Test structured debug message")
|
|
||||||
testLgr.WithField("service", "test").WithField("status", "ok").Debug("Test multi-field message")
|
|
||||||
|
|
||||||
// Test warning logging
|
|
||||||
os.Setenv("DEBUG_I2P", "warn")
|
|
||||||
testLgr = logger.GetGoI2PLogger()
|
|
||||||
testLgr.Warn("Test warning message")
|
|
||||||
|
|
||||||
// Test error logging
|
|
||||||
os.Setenv("DEBUG_I2P", "error")
|
|
||||||
testLgr = logger.GetGoI2PLogger()
|
|
||||||
testLgr.WithField("error_type", "test").Error("Test error message")
|
|
||||||
|
|
||||||
// Test that logging is disabled by default
|
|
||||||
os.Setenv("DEBUG_I2P", "")
|
|
||||||
testLgr = logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
// These should be no-ops when logging is disabled
|
|
||||||
testLgr.Debug("This should not appear")
|
|
||||||
testLgr.Warn("This should not appear")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStructuredLogging verifies the structured logging patterns used throughout the codebase
|
|
||||||
func TestStructuredLogging(t *testing.T) {
|
|
||||||
// Set up debug logging for this test
|
|
||||||
os.Setenv("DEBUG_I2P", "debug")
|
|
||||||
defer os.Setenv("DEBUG_I2P", "")
|
|
||||||
|
|
||||||
testLgr := logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
// Test common patterns used in the codebase
|
|
||||||
testLgr.WithField("service", "test").Debug("Service starting")
|
|
||||||
testLgr.WithField("address", "127.0.0.1:8080").Debug("Server started")
|
|
||||||
testLgr.WithField("protocol", "https").Debug("Protocol configured")
|
|
||||||
|
|
||||||
// Test error patterns
|
|
||||||
testErr := &testError{message: "test error"}
|
|
||||||
testLgr.WithError(testErr).Error("Test error handling")
|
|
||||||
testLgr.WithError(testErr).WithField("context", "test").Error("Test error with context")
|
|
||||||
|
|
||||||
// Test performance logging patterns
|
|
||||||
testLgr.WithField("total_allocs_kb", 1024).WithField("num_gc", 5).Debug("Memory stats")
|
|
||||||
|
|
||||||
// Test I2P-specific patterns
|
|
||||||
testLgr.WithField("sam_address", "127.0.0.1:7656").Debug("SAM connection configured")
|
|
||||||
testLgr.WithField("netdb_path", "/tmp/test").Debug("NetDB path configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// testError implements error interface for testing
|
|
||||||
type testError struct {
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *testError) Error() string {
|
|
||||||
return e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkLoggingOverhead measures the performance impact of logging when disabled
|
|
||||||
func BenchmarkLoggingOverhead(b *testing.B) {
|
|
||||||
// Ensure logging is disabled
|
|
||||||
os.Setenv("DEBUG_I2P", "")
|
|
||||||
defer os.Setenv("DEBUG_I2P", "")
|
|
||||||
|
|
||||||
testLgr := logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
testLgr.WithField("iteration", i).Debug("Benchmark test message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkLoggingEnabled measures the performance impact of logging when enabled
|
|
||||||
func BenchmarkLoggingEnabled(b *testing.B) {
|
|
||||||
// Enable debug logging
|
|
||||||
os.Setenv("DEBUG_I2P", "debug")
|
|
||||||
defer os.Setenv("DEBUG_I2P", "")
|
|
||||||
|
|
||||||
testLgr := logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
testLgr.WithField("iteration", i).Debug("Benchmark test message")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,6 +2,8 @@ package reseed
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,24 +12,20 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ping tests the availability of a reseed server by requesting an SU3 file.
|
// Ping requests an ".su3" from another reseed server and return true if
|
||||||
// It appends "i2pseeds.su3" to the URL if not present and validates the server response.
|
// the reseed server is alive If the reseed server is not alive, returns
|
||||||
// Returns true if the server responds with HTTP 200, false and error details otherwise.
|
// false and the status of the request as an error
|
||||||
// Example usage: alive, err := Ping("https://reseed.example.com/")
|
|
||||||
func Ping(urlInput string) (bool, error) {
|
func Ping(urlInput string) (bool, error) {
|
||||||
// Ensure URL targets the standard reseed SU3 file endpoint
|
|
||||||
if !strings.HasSuffix(urlInput, "i2pseeds.su3") {
|
if !strings.HasSuffix(urlInput, "i2pseeds.su3") {
|
||||||
urlInput = fmt.Sprintf("%s%s", urlInput, "i2pseeds.su3")
|
urlInput = fmt.Sprintf("%s%s", urlInput, "i2pseeds.su3")
|
||||||
}
|
}
|
||||||
lgr.WithField("url", urlInput).Debug("Pinging reseed server")
|
log.Println("Pinging:", urlInput)
|
||||||
// Create HTTP request with proper User-Agent for I2P compatibility
|
|
||||||
req, err := http.NewRequest("GET", urlInput, nil)
|
req, err := http.NewRequest("GET", urlInput, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", I2pUserAgent)
|
req.Header.Set("User-Agent", I2pUserAgent)
|
||||||
|
|
||||||
// Execute request and check for successful response
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -40,72 +38,80 @@ func Ping(urlInput string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func trimPath(s string) string {
|
func trimPath(s string) string {
|
||||||
// Remove protocol and path components to create clean filename
|
|
||||||
tmp := strings.ReplaceAll(s, "https://", "")
|
tmp := strings.ReplaceAll(s, "https://", "")
|
||||||
tmp = strings.ReplaceAll(tmp, "http://", "")
|
tmp = strings.ReplaceAll(s, "http://", "")
|
||||||
tmp = strings.ReplaceAll(tmp, "/", "")
|
tmp = strings.ReplaceAll(s, "/", "")
|
||||||
return tmp
|
return tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
// PingWriteContent performs a ping test and writes the result to a timestamped file.
|
|
||||||
// Creates daily ping status files in the content directory for status tracking and
|
|
||||||
// web interface display. Files are named with host and date to prevent conflicts.
|
|
||||||
func PingWriteContent(urlInput string) error {
|
func PingWriteContent(urlInput string) error {
|
||||||
lgr.WithField("url", urlInput).Debug("Calling PWC")
|
log.Println("Calling PWC", urlInput)
|
||||||
// Generate date stamp for daily ping file organization
|
|
||||||
date := time.Now().Format("2006-01-02")
|
date := time.Now().Format("2006-01-02")
|
||||||
u, err := url.Parse(urlInput)
|
u, err := url.Parse(urlInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).WithField("url", urlInput).Error("PWC URL parsing error")
|
log.Println("PWC", err)
|
||||||
return fmt.Errorf("PingWriteContent:%s", err)
|
return fmt.Errorf("PingWriteContent:%s", err)
|
||||||
}
|
}
|
||||||
// Create clean filename from host and date for ping result storage
|
|
||||||
path := trimPath(u.Host)
|
path := trimPath(u.Host)
|
||||||
lgr.WithField("path", path).Debug("Calling PWC path")
|
log.Println("Calling PWC path", path)
|
||||||
BaseContentPath, _ := StableContentPath()
|
BaseContentPath, _ := StableContentPath()
|
||||||
path = filepath.Join(BaseContentPath, path+"-"+date+".ping")
|
path = filepath.Join(BaseContentPath, path+"-"+date+".ping")
|
||||||
// Only ping if daily result file doesn't exist to prevent spam
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
result, err := Ping(urlInput)
|
result, err := Ping(urlInput)
|
||||||
if result {
|
if result {
|
||||||
lgr.WithField("url", urlInput).Debug("Ping: OK")
|
log.Printf("Ping: %s OK", urlInput)
|
||||||
err := os.WriteFile(path, []byte("Alive: Status OK"), 0o644)
|
err := ioutil.WriteFile(path, []byte("Alive: Status OK"), 0o644)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
lgr.WithField("url", urlInput).WithError(err).Error("Ping: failed")
|
log.Printf("Ping: %s %s", urlInput, err)
|
||||||
err := os.WriteFile(path, []byte("Dead: "+err.Error()), 0o644)
|
err := ioutil.WriteFile(path, []byte("Dead: "+err.Error()), 0o644)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllReseeds moved to shared_utils.go
|
// TODO: make this a configuration option
|
||||||
|
/*var AllReseeds = []string{
|
||||||
|
"https://banana.incognet.io/",
|
||||||
|
"https://i2p.novg.net/",
|
||||||
|
"https://i2pseed.creativecowpat.net:8443/",
|
||||||
|
"https://reseed.diva.exchange/",
|
||||||
|
"https://reseed.i2pgit.org/",
|
||||||
|
"https://reseed.memcpy.io/",
|
||||||
|
"https://reseed.onion.im/",
|
||||||
|
"https://reseed2.i2p.net/",
|
||||||
|
}*/
|
||||||
|
|
||||||
|
var AllReseeds = []string{
|
||||||
|
"https://banana.incognet.io/",
|
||||||
|
"https://i2p.novg.net/",
|
||||||
|
"https://i2pseed.creativecowpat.net:8443/",
|
||||||
|
"https://reseed-fr.i2pd.xyz/",
|
||||||
|
"https://reseed-pl.i2pd.xyz/",
|
||||||
|
"https://reseed.diva.exchange/",
|
||||||
|
"https://reseed.i2pgit.org/",
|
||||||
|
"https://reseed.memcpy.io/",
|
||||||
|
"https://reseed.onion.im/",
|
||||||
|
"https://reseed2.i2p.net/",
|
||||||
|
"https://www2.mk16.de/",
|
||||||
|
}
|
||||||
|
|
||||||
func yday() time.Time {
|
func yday() time.Time {
|
||||||
// Calculate yesterday's date for rate limiting ping operations
|
|
||||||
today := time.Now()
|
today := time.Now()
|
||||||
yesterday := today.Add(-24 * time.Hour)
|
yesterday := today.Add(-24 * time.Hour)
|
||||||
return yesterday
|
return yesterday
|
||||||
}
|
}
|
||||||
|
|
||||||
// lastPing tracks the timestamp of the last successful ping operation for rate limiting.
|
|
||||||
// This prevents excessive server polling by ensuring ping operations only occur once
|
|
||||||
// per 24-hour period, respecting reseed server resources and network bandwidth.
|
|
||||||
var lastPing = yday()
|
var lastPing = yday()
|
||||||
|
|
||||||
// PingEverybody tests all known reseed servers and returns their status results.
|
|
||||||
// Implements rate limiting to prevent excessive pinging (once per 24 hours) and
|
|
||||||
// returns a slice of status strings indicating success or failure for each server.
|
|
||||||
func PingEverybody() []string {
|
func PingEverybody() []string {
|
||||||
// Enforce rate limiting to prevent server abuse
|
|
||||||
if lastPing.After(yday()) {
|
if lastPing.After(yday()) {
|
||||||
lgr.Debug("Your ping was rate-limited")
|
log.Println("Your ping was rate-limited")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
lastPing = time.Now()
|
lastPing = time.Now()
|
||||||
var nonerrs []string
|
var nonerrs []string
|
||||||
// Test each reseed server and collect results for display
|
|
||||||
for _, urlInput := range AllReseeds {
|
for _, urlInput := range AllReseeds {
|
||||||
err := PingWriteContent(urlInput)
|
err := PingWriteContent(urlInput)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -117,14 +123,11 @@ func PingEverybody() []string {
|
|||||||
return nonerrs
|
return nonerrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPingFiles retrieves all ping result files from today for status display.
|
// Get a list of all files ending in ping in the BaseContentPath
|
||||||
// Searches the content directory for .ping files containing today's date and
|
|
||||||
// returns their paths for processing by the web interface status page.
|
|
||||||
func GetPingFiles() ([]string, error) {
|
func GetPingFiles() ([]string, error) {
|
||||||
var files []string
|
var files []string
|
||||||
date := time.Now().Format("2006-01-02")
|
date := time.Now().Format("2006-01-02")
|
||||||
BaseContentPath, _ := StableContentPath()
|
BaseContentPath, _ := StableContentPath()
|
||||||
// Walk content directory to find today's ping files
|
|
||||||
err := filepath.Walk(BaseContentPath, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(BaseContentPath, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -135,23 +138,19 @@ func GetPingFiles() ([]string, error) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return nil, fmt.Errorf("no ping files found")
|
return nil, fmt.Errorf("No ping files found")
|
||||||
}
|
}
|
||||||
return files, err
|
return files, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadOut writes HTML-formatted ping status information to the HTTP response.
|
|
||||||
// Displays the current status of all known reseed servers in a user-friendly format
|
|
||||||
// for the web interface, including warnings about experimental nature of the feature.
|
|
||||||
func ReadOut(w http.ResponseWriter) {
|
func ReadOut(w http.ResponseWriter) {
|
||||||
pinglist, err := GetPingFiles()
|
pinglist, err := GetPingFiles()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Generate HTML status display with ping results
|
|
||||||
fmt.Fprintf(w, "<h3>Reseed Server Statuses</h3>")
|
fmt.Fprintf(w, "<h3>Reseed Server Statuses</h3>")
|
||||||
fmt.Fprintf(w, "<div class=\"pingtest\">This feature is experimental and may not always provide accurate results.</div>")
|
fmt.Fprintf(w, "<div class=\"pingtest\">This feature is experimental and may not always provide accurate results.</div>")
|
||||||
fmt.Fprintf(w, "<div class=\"homepage\"><p><ul>")
|
fmt.Fprintf(w, "<div class=\"homepage\"><p><ul>")
|
||||||
for _, file := range pinglist {
|
for _, file := range pinglist {
|
||||||
ping, err := os.ReadFile(file)
|
ping, err := ioutil.ReadFile(file)
|
||||||
host := strings.Replace(file, ".ping", "", 1)
|
host := strings.Replace(file, ".ping", "", 1)
|
||||||
host = filepath.Base(host)
|
host = filepath.Base(host)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
335
reseed/server.go
335
reseed/server.go
@@ -2,64 +2,58 @@ package reseed
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-i2p/onramp"
|
"github.com/cretz/bine/tor"
|
||||||
|
"github.com/eyedeekay/i2pkeys"
|
||||||
|
"github.com/eyedeekay/sam3"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/justinas/alice"
|
"github.com/justinas/alice"
|
||||||
throttled "github.com/throttled/throttled/v2"
|
throttled "github.com/throttled/throttled/v2"
|
||||||
"github.com/throttled/throttled/v2/store"
|
"github.com/throttled/throttled/v2/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants moved to constants.go
|
const (
|
||||||
|
I2pUserAgent = "Wget/1.11.4"
|
||||||
|
)
|
||||||
|
|
||||||
// Server represents a complete reseed server instance with multi-protocol support.
|
|
||||||
// It provides HTTP/HTTPS reseed services over clearnet, I2P, and Tor networks with
|
|
||||||
// rate limiting, blacklisting, and comprehensive security features for distributing
|
|
||||||
// router information to bootstrap new I2P nodes joining the network.
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*http.Server
|
*http.Server
|
||||||
|
I2P *sam3.SAM
|
||||||
// Reseeder handles the core reseed functionality and SU3 file generation
|
I2PSession *sam3.StreamSession
|
||||||
Reseeder *ReseederImpl
|
I2PListener *sam3.StreamListener
|
||||||
// Blacklist manages IP-based access control for security
|
I2PKeys i2pkeys.I2PKeys
|
||||||
Blacklist *Blacklist
|
Reseeder *ReseederImpl
|
||||||
|
Blacklist *Blacklist
|
||||||
// ServerListener handles standard HTTP/HTTPS connections
|
OnionListener *tor.OnionService
|
||||||
ServerListener net.Listener
|
|
||||||
|
|
||||||
// I2P Listener configuration for serving over I2P network
|
|
||||||
Garlic *onramp.Garlic
|
|
||||||
I2PListener net.Listener
|
|
||||||
|
|
||||||
// Tor Listener configuration for serving over Tor network
|
|
||||||
OnionListener net.Listener
|
|
||||||
Onion *onramp.Onion
|
|
||||||
|
|
||||||
// Rate limiting configuration for request throttling
|
|
||||||
RequestRateLimit int
|
RequestRateLimit int
|
||||||
WebRateLimit int
|
WebRateLimit int
|
||||||
// Thread-safe tracking of acceptable client connection timing
|
|
||||||
acceptables map[string]time.Time
|
acceptables map[string]time.Time
|
||||||
acceptablesMutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new reseed server instance with secure TLS configuration.
|
|
||||||
// It sets up TLS 1.3-only connections, proper cipher suites, and middleware chain for
|
|
||||||
// request processing. The prefix parameter customizes URL paths and trustProxy enables
|
|
||||||
// reverse proxy support for deployment behind load balancers or CDNs.
|
|
||||||
func NewServer(prefix string, trustProxy bool) *Server {
|
func NewServer(prefix string, trustProxy bool) *Server {
|
||||||
config := &tls.Config{
|
config := &tls.Config{
|
||||||
|
// MinVersion: tls.VersionTLS10,
|
||||||
|
// PreferServerCipherSuites: true,
|
||||||
|
// CipherSuites: []uint16{
|
||||||
|
// tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
// tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
// tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
// tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
// tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||||
|
// tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||||
|
// },
|
||||||
MinVersion: tls.VersionTLS13,
|
MinVersion: tls.VersionTLS13,
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
@@ -82,7 +76,7 @@ func NewServer(prefix string, trustProxy bool) *Server {
|
|||||||
errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
if _, err := w.Write(nil); nil != err {
|
if _, err := w.Write(nil); nil != err {
|
||||||
lgr.WithError(err).Error("Error writing HTTP response")
|
log.Println(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,23 +90,20 @@ func NewServer(prefix string, trustProxy bool) *Server {
|
|||||||
|
|
||||||
// See use of crypto/rand on:
|
// See use of crypto/rand on:
|
||||||
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
||||||
// Constants moved to constants.go
|
const (
|
||||||
|
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 52 possibilities
|
||||||
|
letterIdxBits = 6 // 6 bits to represent 64 possibilities / indexes
|
||||||
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||||
|
)
|
||||||
|
|
||||||
// SecureRandomAlphaString generates a cryptographically secure random alphabetic string.
|
|
||||||
// Returns a 16-character string using only letters for use in tokens, session IDs, and
|
|
||||||
// other security-sensitive contexts. Uses crypto/rand for entropy source.
|
|
||||||
func SecureRandomAlphaString() string {
|
func SecureRandomAlphaString() string {
|
||||||
// Fixed 16-character length for consistent token generation
|
|
||||||
length := 16
|
length := 16
|
||||||
result := make([]byte, length)
|
result := make([]byte, length)
|
||||||
// Buffer size calculation for efficient random byte usage
|
|
||||||
bufferSize := int(float64(length) * 1.3)
|
bufferSize := int(float64(length) * 1.3)
|
||||||
for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
|
for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
|
||||||
// Refresh random bytes buffer when needed for efficiency
|
|
||||||
if j%bufferSize == 0 {
|
if j%bufferSize == 0 {
|
||||||
randomBytes = SecureRandomBytes(bufferSize)
|
randomBytes = SecureRandomBytes(bufferSize)
|
||||||
}
|
}
|
||||||
// Filter random bytes to only include valid letter indices
|
|
||||||
if idx := int(randomBytes[j%length] & letterIdxMask); idx < len(letterBytes) {
|
if idx := int(randomBytes[j%length] & letterIdxMask); idx < len(letterBytes) {
|
||||||
result[i] = letterBytes[idx]
|
result[i] = letterBytes[idx]
|
||||||
i++
|
i++
|
||||||
@@ -121,65 +112,44 @@ func SecureRandomAlphaString() string {
|
|||||||
return string(result)
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureRandomBytes generates cryptographically secure random bytes of specified length.
|
// SecureRandomBytes returns the requested number of bytes using crypto/rand
|
||||||
// Uses crypto/rand for high-quality entropy suitable for cryptographic operations, tokens,
|
|
||||||
// and security-sensitive random data generation. Panics on randomness failure for security.
|
|
||||||
func SecureRandomBytes(length int) []byte {
|
func SecureRandomBytes(length int) []byte {
|
||||||
randomBytes := make([]byte, length)
|
randomBytes := make([]byte, length)
|
||||||
// Use crypto/rand for cryptographically secure random generation
|
|
||||||
_, err := rand.Read(randomBytes)
|
_, err := rand.Read(randomBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).Fatal("Unable to generate random bytes")
|
log.Fatal("Unable to generate random bytes")
|
||||||
}
|
}
|
||||||
return randomBytes
|
return randomBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
func (srv *Server) Address() string {
|
|
||||||
addrs := make(map[string]string)
|
|
||||||
if srv.I2PListener != nil {
|
|
||||||
addrs["i2p"] = srv.I2PListener.Addr().String()
|
|
||||||
}
|
|
||||||
if srv.OnionListener != nil {
|
|
||||||
addrs["onion"] = srv.OnionListener.Addr().String()
|
|
||||||
}
|
|
||||||
if srv.Server != nil {
|
|
||||||
addrs["tcp"] = srv.Server.Addr
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%v", addrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) Acceptable() string {
|
func (srv *Server) Acceptable() string {
|
||||||
srv.acceptablesMutex.Lock()
|
|
||||||
defer srv.acceptablesMutex.Unlock()
|
|
||||||
|
|
||||||
if srv.acceptables == nil {
|
if srv.acceptables == nil {
|
||||||
srv.acceptables = make(map[string]time.Time)
|
srv.acceptables = make(map[string]time.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up expired entries first
|
|
||||||
srv.cleanupExpiredTokensUnsafe()
|
|
||||||
|
|
||||||
// If still too many entries, remove oldest ones
|
|
||||||
if len(srv.acceptables) > 50 {
|
if len(srv.acceptables) > 50 {
|
||||||
srv.evictOldestTokensUnsafe(50)
|
for val := range srv.acceptables {
|
||||||
|
srv.CheckAcceptable(val)
|
||||||
|
}
|
||||||
|
for val := range srv.acceptables {
|
||||||
|
if len(srv.acceptables) < 50 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delete(srv.acceptables, val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptme := SecureRandomAlphaString()
|
acceptme := SecureRandomAlphaString()
|
||||||
srv.acceptables[acceptme] = time.Now()
|
srv.acceptables[acceptme] = time.Now()
|
||||||
return acceptme
|
return acceptme
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Server) CheckAcceptable(val string) bool {
|
func (srv *Server) CheckAcceptable(val string) bool {
|
||||||
srv.acceptablesMutex.Lock()
|
|
||||||
defer srv.acceptablesMutex.Unlock()
|
|
||||||
|
|
||||||
if srv.acceptables == nil {
|
if srv.acceptables == nil {
|
||||||
srv.acceptables = make(map[string]time.Time)
|
srv.acceptables = make(map[string]time.Time)
|
||||||
}
|
}
|
||||||
if timeout, ok := srv.acceptables[val]; ok {
|
if timeout, ok := srv.acceptables[val]; ok {
|
||||||
checktime := time.Since(timeout)
|
checktime := time.Now().Sub(timeout)
|
||||||
if checktime > (4 * time.Minute) {
|
if checktime > (4 * time.Minute) {
|
||||||
delete(srv.acceptables, val)
|
delete(srv.acceptables, val)
|
||||||
return false
|
return false
|
||||||
@@ -190,19 +160,167 @@ func (srv *Server) CheckAcceptable(val string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAcceptableUnsafe performs acceptable checking without acquiring the mutex.
|
func (srv *Server) ListenAndServe() error {
|
||||||
// This should only be called when the mutex is already held.
|
addr := srv.Addr
|
||||||
func (srv *Server) checkAcceptableUnsafe(val string) bool {
|
if addr == "" {
|
||||||
if timeout, ok := srv.acceptables[val]; ok {
|
addr = ":http"
|
||||||
checktime := time.Since(timeout)
|
|
||||||
if checktime > (4 * time.Minute) {
|
|
||||||
delete(srv.acceptables, val)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Don't delete here since we're just cleaning up expired entries
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
return false
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.Serve(newBlacklistListener(ln, srv.Blacklist))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error {
|
||||||
|
addr := srv.Addr
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":https"
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.TLSConfig == nil {
|
||||||
|
srv.TLSConfig = &tls.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.TLSConfig.NextProtos == nil {
|
||||||
|
srv.TLSConfig.NextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
srv.TLSConfig.Certificates = make([]tls.Certificate, 1)
|
||||||
|
srv.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsListener := tls.NewListener(newBlacklistListener(ln, srv.Blacklist), srv.TLSConfig)
|
||||||
|
return srv.Serve(tlsListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) ListenAndServeOnionTLS(startConf *tor.StartConf, listenConf *tor.ListenConf, certFile, keyFile string) error {
|
||||||
|
log.Println("Starting and registering OnionV3 HTTPS service, please wait a couple of minutes...")
|
||||||
|
tor, err := tor.Start(nil, startConf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tor.Close()
|
||||||
|
|
||||||
|
listenCtx, listenCancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
|
defer listenCancel()
|
||||||
|
|
||||||
|
srv.OnionListener, err = tor.Listen(listenCtx, listenConf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.Addr = srv.OnionListener.ID
|
||||||
|
if srv.TLSConfig == nil {
|
||||||
|
srv.TLSConfig = &tls.Config{
|
||||||
|
ServerName: srv.OnionListener.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.TLSConfig.NextProtos == nil {
|
||||||
|
srv.TLSConfig.NextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// var err error
|
||||||
|
srv.TLSConfig.Certificates = make([]tls.Certificate, 1)
|
||||||
|
srv.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Onionv3 server started on https://%v.onion\n", srv.OnionListener.ID)
|
||||||
|
|
||||||
|
// tlsListener := tls.NewListener(newBlacklistListener(srv.OnionListener, srv.Blacklist), srv.TLSConfig)
|
||||||
|
tlsListener := tls.NewListener(srv.OnionListener, srv.TLSConfig)
|
||||||
|
|
||||||
|
return srv.Serve(tlsListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) ListenAndServeOnion(startConf *tor.StartConf, listenConf *tor.ListenConf) error {
|
||||||
|
log.Println("Starting and registering OnionV3 service, please wait a couple of minutes...")
|
||||||
|
tor, err := tor.Start(nil, startConf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tor.Close()
|
||||||
|
|
||||||
|
listenCtx, listenCancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
|
defer listenCancel()
|
||||||
|
srv.OnionListener, err = tor.Listen(listenCtx, listenConf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Onionv3 server started on http://%v.onion\n", srv.OnionListener.ID)
|
||||||
|
return srv.Serve(srv.OnionListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) ListenAndServeI2PTLS(samaddr string, I2PKeys i2pkeys.I2PKeys, certFile, keyFile string) error {
|
||||||
|
log.Println("Starting and registering I2P HTTPS service, please wait a couple of minutes...")
|
||||||
|
var err error
|
||||||
|
srv.I2P, err = sam3.NewSAM(samaddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.I2PSession, err = srv.I2P.NewStreamSession("", I2PKeys, []string{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.I2PListener, err = srv.I2PSession.Listen()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.Addr = srv.I2PListener.Addr().(i2pkeys.I2PAddr).Base32()
|
||||||
|
if srv.TLSConfig == nil {
|
||||||
|
srv.TLSConfig = &tls.Config{
|
||||||
|
ServerName: srv.I2PListener.Addr().(i2pkeys.I2PAddr).Base32(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.TLSConfig.NextProtos == nil {
|
||||||
|
srv.TLSConfig.NextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// var err error
|
||||||
|
srv.TLSConfig.Certificates = make([]tls.Certificate, 1)
|
||||||
|
srv.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("I2P server started on https://%v\n", srv.I2PListener.Addr().(i2pkeys.I2PAddr).Base32())
|
||||||
|
|
||||||
|
// tlsListener := tls.NewListener(newBlacklistListener(srv.OnionListener, srv.Blacklist), srv.TLSConfig)
|
||||||
|
tlsListener := tls.NewListener(srv.I2PListener, srv.TLSConfig)
|
||||||
|
|
||||||
|
return srv.Serve(tlsListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) ListenAndServeI2P(samaddr string, I2PKeys i2pkeys.I2PKeys) error {
|
||||||
|
log.Println("Starting and registering I2P service, please wait a couple of minutes...")
|
||||||
|
var err error
|
||||||
|
srv.I2P, err = sam3.NewSAM(samaddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.I2PSession, err = srv.I2P.NewStreamSession("", I2PKeys, []string{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.I2PListener, err = srv.I2PSession.Listen()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("I2P server started on http://%v.b32.i2p\n", srv.I2PListener.Addr().(i2pkeys.I2PAddr).Base32())
|
||||||
|
return srv.Serve(srv.I2PListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Server) reseedHandler(w http.ResponseWriter, r *http.Request) {
|
func (srv *Server) reseedHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -215,7 +333,6 @@ func (srv *Server) reseedHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
su3Bytes, err := srv.Reseeder.PeerSu3Bytes(peer)
|
su3Bytes, err := srv.Reseeder.PeerSu3Bytes(peer)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).WithField("peer", peer).Errorf("Error serving su3 %s", err)
|
|
||||||
http.Error(w, "500 Unable to serve su3", http.StatusInternalServerError)
|
http.Error(w, "500 Unable to serve su3", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -244,7 +361,6 @@ func (srv *Server) browsingMiddleware(next http.Handler) http.Handler {
|
|||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
if srv.CheckAcceptable(r.FormValue("onetime")) {
|
if srv.CheckAcceptable(r.FormValue("onetime")) {
|
||||||
srv.reseedHandler(w, r)
|
srv.reseedHandler(w, r)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if I2pUserAgent != r.UserAgent() {
|
if I2pUserAgent != r.UserAgent() {
|
||||||
srv.HandleARealBrowser(w, r)
|
srv.HandleARealBrowser(w, r)
|
||||||
@@ -277,44 +393,3 @@ func proxiedMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
return http.HandlerFunc(fn)
|
return http.HandlerFunc(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanupExpiredTokensUnsafe removes expired tokens from the acceptables map.
|
|
||||||
// This should only be called when the mutex is already held.
|
|
||||||
func (srv *Server) cleanupExpiredTokensUnsafe() {
|
|
||||||
now := time.Now()
|
|
||||||
for token, timestamp := range srv.acceptables {
|
|
||||||
if now.Sub(timestamp) > (4 * time.Minute) {
|
|
||||||
delete(srv.acceptables, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// evictOldestTokensUnsafe removes the oldest tokens to keep the map size at the target.
|
|
||||||
// This should only be called when the mutex is already held.
|
|
||||||
func (srv *Server) evictOldestTokensUnsafe(targetSize int) {
|
|
||||||
if len(srv.acceptables) <= targetSize {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to slice and sort by timestamp
|
|
||||||
type tokenTime struct {
|
|
||||||
token string
|
|
||||||
time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens := make([]tokenTime, 0, len(srv.acceptables))
|
|
||||||
for token, timestamp := range srv.acceptables {
|
|
||||||
tokens = append(tokens, tokenTime{token, timestamp})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by timestamp (oldest first)
|
|
||||||
sort.Slice(tokens, func(i, j int) bool {
|
|
||||||
return tokens[i].time.Before(tokens[j].time)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Delete oldest tokens until we reach target size
|
|
||||||
toDelete := len(srv.acceptables) - targetSize
|
|
||||||
for i := 0; i < toDelete && i < len(tokens); i++ {
|
|
||||||
delete(srv.acceptables, tokens[i].token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,209 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test for Bug #3: Unbounded Memory Growth in Acceptable Tokens (FIXED)
|
|
||||||
func TestAcceptableTokensMemoryBounds(t *testing.T) {
|
|
||||||
server := &Server{}
|
|
||||||
|
|
||||||
// Test 1: Verify tokens are cleaned up after expiration
|
|
||||||
t.Run("ExpiredTokenCleanup", func(t *testing.T) {
|
|
||||||
// Create some tokens and artificially age them
|
|
||||||
server.acceptables = make(map[string]time.Time)
|
|
||||||
oldTime := time.Now().Add(-5 * time.Minute) // Older than 4-minute expiry
|
|
||||||
recentTime := time.Now()
|
|
||||||
|
|
||||||
server.acceptables["old_token_1"] = oldTime
|
|
||||||
server.acceptables["old_token_2"] = oldTime
|
|
||||||
server.acceptables["recent_token"] = recentTime
|
|
||||||
|
|
||||||
if len(server.acceptables) != 3 {
|
|
||||||
t.Errorf("Expected 3 tokens initially, got %d", len(server.acceptables))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger cleanup by calling Acceptable
|
|
||||||
_ = server.Acceptable()
|
|
||||||
|
|
||||||
// Check that old tokens were cleaned up but recent one remains
|
|
||||||
if len(server.acceptables) > 2 {
|
|
||||||
t.Errorf("Expected at most 2 tokens after cleanup, got %d", len(server.acceptables))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify recent token still exists
|
|
||||||
if _, exists := server.acceptables["recent_token"]; !exists {
|
|
||||||
t.Error("Recent token should not have been cleaned up")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify old tokens were removed
|
|
||||||
if _, exists := server.acceptables["old_token_1"]; exists {
|
|
||||||
t.Error("Old token should have been cleaned up")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test 2: Verify size-based eviction when too many tokens
|
|
||||||
t.Run("SizeBasedEviction", func(t *testing.T) {
|
|
||||||
server.acceptables = make(map[string]time.Time)
|
|
||||||
|
|
||||||
// Add more than 50 tokens
|
|
||||||
for i := 0; i < 60; i++ {
|
|
||||||
token := server.Acceptable()
|
|
||||||
// Ensure each token has a slightly different timestamp
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
if token == "" {
|
|
||||||
t.Error("Acceptable() should return a valid token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be limited to around 50 tokens due to eviction
|
|
||||||
if len(server.acceptables) > 55 {
|
|
||||||
t.Errorf("Expected token count to be limited, got %d", len(server.acceptables))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test 3: Verify token validation works correctly
|
|
||||||
t.Run("TokenValidation", func(t *testing.T) {
|
|
||||||
server.acceptables = make(map[string]time.Time)
|
|
||||||
|
|
||||||
// Generate a token
|
|
||||||
token := server.Acceptable()
|
|
||||||
if token == "" {
|
|
||||||
t.Fatal("Expected valid token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify token is valid
|
|
||||||
if !server.CheckAcceptable(token) {
|
|
||||||
t.Error("Token should be valid immediately after creation")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify token is consumed (single-use)
|
|
||||||
if server.CheckAcceptable(token) {
|
|
||||||
t.Error("Token should not be valid after first use")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify invalid token returns false
|
|
||||||
if server.CheckAcceptable("invalid_token") {
|
|
||||||
t.Error("Invalid token should return false")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test 4: Verify memory doesn't grow unboundedly
|
|
||||||
t.Run("UnboundedGrowthPrevention", func(t *testing.T) {
|
|
||||||
server.acceptables = make(map[string]time.Time)
|
|
||||||
|
|
||||||
// Generate many tokens without checking them
|
|
||||||
// This was the original bug scenario
|
|
||||||
for i := 0; i < 200; i++ {
|
|
||||||
_ = server.Acceptable()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memory should be bounded
|
|
||||||
if len(server.acceptables) > 60 {
|
|
||||||
t.Errorf("Memory growth not properly bounded: %d tokens", len(server.acceptables))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Token map size after 200 generations: %d (should be bounded)", len(server.acceptables))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test 5: Test concurrent access safety
|
|
||||||
t.Run("ConcurrentAccess", func(t *testing.T) {
|
|
||||||
server.acceptables = make(map[string]time.Time)
|
|
||||||
|
|
||||||
// Launch multiple goroutines generating and checking tokens
|
|
||||||
done := make(chan bool, 4)
|
|
||||||
|
|
||||||
// Token generators
|
|
||||||
go func() {
|
|
||||||
for i := 0; i < 50; i++ {
|
|
||||||
_ = server.Acceptable()
|
|
||||||
}
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for i := 0; i < 50; i++ {
|
|
||||||
_ = server.Acceptable()
|
|
||||||
}
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Token checkers
|
|
||||||
go func() {
|
|
||||||
for i := 0; i < 25; i++ {
|
|
||||||
token := server.Acceptable()
|
|
||||||
_ = server.CheckAcceptable(token)
|
|
||||||
}
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for i := 0; i < 25; i++ {
|
|
||||||
token := server.Acceptable()
|
|
||||||
_ = server.CheckAcceptable(token)
|
|
||||||
}
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for all goroutines to complete
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should not panic and should have bounded size
|
|
||||||
if len(server.acceptables) > 100 {
|
|
||||||
t.Errorf("Concurrent access resulted in unbounded growth: %d tokens", len(server.acceptables))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Token map size after concurrent access: %d", len(server.acceptables))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the cleanup methods directly
|
|
||||||
func TestTokenCleanupMethods(t *testing.T) {
|
|
||||||
server := &Server{
|
|
||||||
acceptables: make(map[string]time.Time),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test cleanupExpiredTokensUnsafe
|
|
||||||
t.Run("CleanupExpired", func(t *testing.T) {
|
|
||||||
now := time.Now()
|
|
||||||
server.acceptables["expired1"] = now.Add(-5 * time.Minute)
|
|
||||||
server.acceptables["expired2"] = now.Add(-6 * time.Minute)
|
|
||||||
server.acceptables["valid"] = now
|
|
||||||
|
|
||||||
server.cleanupExpiredTokensUnsafe()
|
|
||||||
|
|
||||||
if len(server.acceptables) != 1 {
|
|
||||||
t.Errorf("Expected 1 token after cleanup, got %d", len(server.acceptables))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, exists := server.acceptables["valid"]; !exists {
|
|
||||||
t.Error("Valid token should remain after cleanup")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test evictOldestTokensUnsafe
|
|
||||||
t.Run("EvictOldest", func(t *testing.T) {
|
|
||||||
server.acceptables = make(map[string]time.Time)
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
// Add tokens with different timestamps
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
server.acceptables[string(rune('a'+i))] = now.Add(time.Duration(-i) * time.Minute)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evict to keep only 5
|
|
||||||
server.evictOldestTokensUnsafe(5)
|
|
||||||
|
|
||||||
if len(server.acceptables) != 5 {
|
|
||||||
t.Errorf("Expected 5 tokens after eviction, got %d", len(server.acceptables))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The newest tokens should remain
|
|
||||||
if _, exists := server.acceptables["a"]; !exists {
|
|
||||||
t.Error("Newest token should remain after eviction")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@@ -6,21 +6,19 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-i2p/common/router_info"
|
"github.com/go-i2p/go-i2p/lib/common/router_info"
|
||||||
"i2pgit.org/go-i2p/reseed-tools/su3"
|
"i2pgit.org/idk/reseed-tools/su3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// routerInfo holds metadata and content for an individual I2P router information file.
|
|
||||||
// Contains the router filename, modification time, raw data, and parsed RouterInfo structure
|
|
||||||
// used for reseed bundle generation and network database management operations.
|
|
||||||
type routerInfo struct {
|
type routerInfo struct {
|
||||||
Name string
|
Name string
|
||||||
ModTime time.Time
|
ModTime time.Time
|
||||||
@@ -28,13 +26,9 @@ type routerInfo struct {
|
|||||||
RI *router_info.RouterInfo
|
RI *router_info.RouterInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peer represents a unique identifier for an I2P peer requesting reseed data.
|
|
||||||
// It is used to generate deterministic, peer-specific SU3 file contents to ensure
|
|
||||||
// different peers receive different router sets for improved network diversity.
|
|
||||||
type Peer string
|
type Peer string
|
||||||
|
|
||||||
func (p Peer) Hash() int {
|
func (p Peer) Hash() int {
|
||||||
// Generate deterministic hash from peer identifier for consistent SU3 selection
|
|
||||||
b := sha256.Sum256([]byte(p))
|
b := sha256.Sum256([]byte(p))
|
||||||
c := make([]byte, len(b))
|
c := make([]byte, len(b))
|
||||||
copy(c, b[:])
|
copy(c, b[:])
|
||||||
@@ -46,49 +40,42 @@ func (p Peer) Hash() int {
|
|||||||
PeerSu3Bytes(peer Peer) ([]byte, error)
|
PeerSu3Bytes(peer Peer) ([]byte, error)
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
// ReseederImpl implements the core reseed service functionality for generating SU3 files.
|
|
||||||
// It manages router information caching, cryptographic signing, and periodic rebuilding of
|
|
||||||
// reseed data to provide fresh router information to bootstrapping I2P nodes. The service
|
|
||||||
// maintains multiple pre-built SU3 files to efficiently serve concurrent requests.
|
|
||||||
type ReseederImpl struct {
|
type ReseederImpl struct {
|
||||||
// netdb provides access to the local router information database
|
|
||||||
netdb *LocalNetDbImpl
|
netdb *LocalNetDbImpl
|
||||||
// su3s stores pre-built SU3 files for efficient serving using atomic operations
|
su3s chan [][]byte
|
||||||
su3s atomic.Value // stores [][]byte
|
|
||||||
|
|
||||||
// SigningKey contains the RSA private key for SU3 file cryptographic signing
|
SigningKey *rsa.PrivateKey
|
||||||
SigningKey *rsa.PrivateKey
|
SignerID []byte
|
||||||
// SignerID contains the identity string used in SU3 signature verification
|
NumRi int
|
||||||
SignerID []byte
|
|
||||||
// NumRi specifies the number of router infos to include in each SU3 file
|
|
||||||
NumRi int
|
|
||||||
// RebuildInterval determines how often to refresh the SU3 file cache
|
|
||||||
RebuildInterval time.Duration
|
RebuildInterval time.Duration
|
||||||
// NumSu3 specifies the number of pre-built SU3 files to maintain
|
NumSu3 int
|
||||||
NumSu3 int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReseeder creates a new reseed service instance with default configuration.
|
|
||||||
// It initializes the service with standard parameters: 77 router infos per SU3 file
|
|
||||||
// and 90-hour rebuild intervals to balance freshness with server performance.
|
|
||||||
func NewReseeder(netdb *LocalNetDbImpl) *ReseederImpl {
|
func NewReseeder(netdb *LocalNetDbImpl) *ReseederImpl {
|
||||||
rs := &ReseederImpl{
|
return &ReseederImpl{
|
||||||
netdb: netdb,
|
netdb: netdb,
|
||||||
|
su3s: make(chan [][]byte),
|
||||||
NumRi: 77,
|
NumRi: 77,
|
||||||
RebuildInterval: 90 * time.Hour,
|
RebuildInterval: 90 * time.Hour,
|
||||||
}
|
}
|
||||||
// Initialize with empty slice to prevent nil panics
|
|
||||||
rs.su3s.Store([][]byte{})
|
|
||||||
return rs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ReseederImpl) Start() chan bool {
|
func (rs *ReseederImpl) Start() chan bool {
|
||||||
// No need for atomic swapper - atomic.Value handles concurrency
|
// atomic swapper
|
||||||
|
go func() {
|
||||||
|
var m [][]byte
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case m = <-rs.su3s:
|
||||||
|
case rs.su3s <- m:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// init the cache
|
// init the cache
|
||||||
err := rs.rebuild()
|
err := rs.rebuild()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).Error("Error during initial rebuild")
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(rs.RebuildInterval)
|
ticker := time.NewTicker(rs.RebuildInterval)
|
||||||
@@ -99,7 +86,7 @@ func (rs *ReseederImpl) Start() chan bool {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
err := rs.rebuild()
|
err := rs.rebuild()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).Error("Error during periodic rebuild")
|
log.Println(err)
|
||||||
}
|
}
|
||||||
case <-quit:
|
case <-quit:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
@@ -112,12 +99,12 @@ func (rs *ReseederImpl) Start() chan bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ReseederImpl) rebuild() error {
|
func (rs *ReseederImpl) rebuild() error {
|
||||||
lgr.WithField("operation", "rebuild").Debug("Rebuilding su3 cache...")
|
log.Println("Rebuilding su3 cache...")
|
||||||
|
|
||||||
// get all RIs from netdb provider
|
// get all RIs from netdb provider
|
||||||
ris, err := rs.netdb.RouterInfos()
|
ris, err := rs.netdb.RouterInfos()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return fmt.Errorf("unable to get routerInfos: %s", err)
|
return fmt.Errorf("Unable to get routerInfos: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use only 75% of routerInfos
|
// use only 75% of routerInfos
|
||||||
@@ -138,16 +125,16 @@ func (rs *ReseederImpl) rebuild() error {
|
|||||||
for gs := range su3Chan {
|
for gs := range su3Chan {
|
||||||
data, err := gs.MarshalBinary()
|
data, err := gs.MarshalBinary()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return fmt.Errorf("error marshaling gs: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newSu3s = append(newSu3s, data)
|
newSu3s = append(newSu3s, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use this new set of su3s
|
// use this new set of su3s
|
||||||
rs.su3s.Store(newSu3s)
|
rs.su3s <- newSu3s
|
||||||
|
|
||||||
lgr.WithField("operation", "rebuild").Debug("Done rebuilding.")
|
log.Println("Done rebuilding.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -174,7 +161,7 @@ func (rs *ReseederImpl) seedsProducer(ris []routerInfo) <-chan []routerInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lgr.WithField("su3_count", numSu3s).WithField("routerinfos_per_su3", rs.NumRi).WithField("total_routerinfos", lenRis).Debug("Building su3 files")
|
log.Printf("Building %d su3 files each containing %d out of %d routerInfos.\n", numSu3s, rs.NumRi, lenRis)
|
||||||
|
|
||||||
out := make(chan []routerInfo)
|
out := make(chan []routerInfo)
|
||||||
|
|
||||||
@@ -200,7 +187,7 @@ func (rs *ReseederImpl) su3Builder(in <-chan []routerInfo) <-chan *su3.File {
|
|||||||
for seeds := range in {
|
for seeds := range in {
|
||||||
gs, err := rs.createSu3(seeds)
|
gs, err := rs.createSu3(seeds)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).Error("Error creating su3 file")
|
log.Println(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,19 +199,14 @@ func (rs *ReseederImpl) su3Builder(in <-chan []routerInfo) <-chan *su3.File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ReseederImpl) PeerSu3Bytes(peer Peer) ([]byte, error) {
|
func (rs *ReseederImpl) PeerSu3Bytes(peer Peer) ([]byte, error) {
|
||||||
m := rs.su3s.Load().([][]byte)
|
m := <-rs.su3s
|
||||||
|
defer func() { rs.su3s <- m }()
|
||||||
|
|
||||||
if len(m) == 0 {
|
if 0 == len(m) {
|
||||||
return nil, errors.New("502: Internal service error, no reseed file available")
|
return nil, errors.New("404")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional safety: ensure index is valid (defense in depth)
|
return m[peer.Hash()%len(m)], nil
|
||||||
index := int(peer.Hash()) % len(m)
|
|
||||||
if index < 0 || index >= len(m) {
|
|
||||||
return nil, errors.New("404: Reseed file not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return m[index], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ReseederImpl) createSu3(seeds []routerInfo) (*su3.File, error) {
|
func (rs *ReseederImpl) createSu3(seeds []routerInfo) (*su3.File, error) {
|
||||||
@@ -249,24 +231,13 @@ func (rs *ReseederImpl) createSu3(seeds []routerInfo) (*su3.File, error) {
|
|||||||
RouterInfos() ([]routerInfo, error)
|
RouterInfos() ([]routerInfo, error)
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
// LocalNetDbImpl provides access to the local I2P router information database.
|
|
||||||
// It manages reading and filtering router info files from the filesystem, applying
|
|
||||||
// age-based filtering to ensure only recent and valid router information is included
|
|
||||||
// in reseed packages distributed to new I2P nodes joining the network.
|
|
||||||
type LocalNetDbImpl struct {
|
type LocalNetDbImpl struct {
|
||||||
// Path specifies the filesystem location of the router information database
|
|
||||||
Path string
|
Path string
|
||||||
// MaxRouterInfoAge defines the maximum age for including router info in reseeds
|
|
||||||
MaxRouterInfoAge time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLocalNetDb creates a new local router database instance with specified parameters.
|
func NewLocalNetDb(path string) *LocalNetDbImpl {
|
||||||
// The path should point to an I2P netDb directory containing routerInfo files, and maxAge
|
|
||||||
// determines how old router information can be before it's excluded from reseed packages.
|
|
||||||
func NewLocalNetDb(path string, maxAge time.Duration) *LocalNetDbImpl {
|
|
||||||
return &LocalNetDbImpl{
|
return &LocalNetDbImpl{
|
||||||
Path: path,
|
Path: path,
|
||||||
MaxRouterInfoAge: maxAge,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,48 +255,41 @@ func (db *LocalNetDbImpl) RouterInfos() (routerInfos []routerInfo, err error) {
|
|||||||
filepath.Walk(db.Path, walkpath)
|
filepath.Walk(db.Path, walkpath)
|
||||||
|
|
||||||
for path, file := range files {
|
for path, file := range files {
|
||||||
riBytes, err := os.ReadFile(path)
|
riBytes, err := ioutil.ReadFile(path)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).WithField("path", path).Error("Error reading RouterInfo file")
|
log.Println(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore outdate routerInfos
|
// ignore outdate routerInfos
|
||||||
age := time.Since(file.ModTime())
|
age := time.Since(file.ModTime())
|
||||||
if age > db.MaxRouterInfoAge {
|
if age.Hours() > 192 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
riStruct, remainder, err := router_info.ReadRouterInfo(riBytes)
|
riStruct, remainder, err := router_info.NewRouterInfo(riBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).WithField("path", path).Error("RouterInfo Parsing Error")
|
log.Println("RouterInfo Parsing Error:", err)
|
||||||
lgr.WithField("path", path).WithField("remainder", remainder).Debug("Leftover Data(for debugging)")
|
log.Println("Leftover Data(for debugging):", remainder)
|
||||||
|
riStruct = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip crappy routerInfos (temporarily bypass GoodVersion check)
|
// skip crappy routerInfos
|
||||||
// TEMPORARY: Accept all reachable routers regardless of version
|
if riStruct.Reachable() && riStruct.UnCongested() && riStruct.GoodVersion() {
|
||||||
gv, err := riStruct.GoodVersion()
|
|
||||||
if err != nil {
|
|
||||||
lgr.WithError(err).WithField("path", path).Error("RouterInfo GoodVersion Error")
|
|
||||||
}
|
|
||||||
if riStruct.Reachable() && riStruct.UnCongested() && gv {
|
|
||||||
routerInfos = append(routerInfos, routerInfo{
|
routerInfos = append(routerInfos, routerInfo{
|
||||||
Name: file.Name(),
|
Name: file.Name(),
|
||||||
ModTime: file.ModTime(),
|
ModTime: file.ModTime(),
|
||||||
Data: riBytes,
|
Data: riBytes,
|
||||||
RI: &riStruct,
|
RI: riStruct,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
lgr.WithField("path", path).WithField("capabilities", riStruct.RouterCapabilities()).WithField("version", riStruct.RouterVersion()).Debug("Skipped less-useful RouterInfo")
|
log.Println("Skipped less-useful RouterInfo Capabilities:", riStruct.RouterCapabilities(), riStruct.RouterVersion())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// fanIn multiplexes multiple SU3 file channels into a single output channel.
|
|
||||||
// This function implements the fan-in concurrency pattern to efficiently merge
|
|
||||||
// multiple concurrent SU3 file generation streams for balanced load distribution.
|
|
||||||
func fanIn(inputs ...<-chan *su3.File) <-chan *su3.File {
|
func fanIn(inputs ...<-chan *su3.File) <-chan *su3.File {
|
||||||
out := make(chan *su3.File, len(inputs))
|
out := make(chan *su3.File, len(inputs))
|
||||||
|
|
||||||
|
@@ -1,260 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLocalNetDb_ConfigurableRouterInfoAge(t *testing.T) {
|
|
||||||
// Create a temporary directory for test
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Create test router info files with different ages
|
|
||||||
files := []struct {
|
|
||||||
name string
|
|
||||||
age time.Duration
|
|
||||||
}{
|
|
||||||
{"routerInfo-test1.dat", 24 * time.Hour}, // 1 day old
|
|
||||||
{"routerInfo-test2.dat", 48 * time.Hour}, // 2 days old
|
|
||||||
{"routerInfo-test3.dat", 96 * time.Hour}, // 4 days old
|
|
||||||
{"routerInfo-test4.dat", 168 * time.Hour}, // 7 days old
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create test files with specific modification times
|
|
||||||
now := time.Now()
|
|
||||||
for _, file := range files {
|
|
||||||
filePath := filepath.Join(tempDir, file.name)
|
|
||||||
err := os.WriteFile(filePath, []byte("dummy router info data"), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test file %s: %v", file.name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set modification time to simulate age
|
|
||||||
modTime := now.Add(-file.age)
|
|
||||||
err = os.Chtimes(filePath, modTime, modTime)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to set mod time for %s: %v", file.name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
maxAge time.Duration
|
|
||||||
expectedFiles int
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "72 hour limit (I2P standard)",
|
|
||||||
maxAge: 72 * time.Hour,
|
|
||||||
expectedFiles: 2, // Files aged 24h and 48h should be included
|
|
||||||
description: "Should include files up to 72 hours old",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "192 hour limit (legacy compatibility)",
|
|
||||||
maxAge: 192 * time.Hour,
|
|
||||||
expectedFiles: 4, // All files should be included
|
|
||||||
description: "Should include files up to 192 hours old (for backwards compatibility)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "36 hour limit (strict)",
|
|
||||||
maxAge: 36 * time.Hour,
|
|
||||||
expectedFiles: 1, // Only the 24h file should be included
|
|
||||||
description: "Should include only files up to 36 hours old",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// Create LocalNetDb with configurable max age
|
|
||||||
netdb := NewLocalNetDb(tempDir, tc.maxAge)
|
|
||||||
|
|
||||||
// Note: RouterInfos() method will try to parse the dummy data and likely fail
|
|
||||||
// since it's not real router info data. But we can still test the age filtering
|
|
||||||
// by checking that it at least attempts to process the right number of files.
|
|
||||||
|
|
||||||
// For this test, we'll just verify that the MaxRouterInfoAge field is set correctly
|
|
||||||
if netdb.MaxRouterInfoAge != tc.maxAge {
|
|
||||||
t.Errorf("Expected MaxRouterInfoAge %v, got %v", tc.maxAge, netdb.MaxRouterInfoAge)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the path is set correctly too
|
|
||||||
if netdb.Path != tempDir {
|
|
||||||
t.Errorf("Expected Path %s, got %s", tempDir, netdb.Path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLocalNetDb_DefaultValues(t *testing.T) {
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Test with different duration values
|
|
||||||
testDurations := []time.Duration{
|
|
||||||
72 * time.Hour, // 3 days (I2P standard default)
|
|
||||||
192 * time.Hour, // 8 days (legacy compatibility)
|
|
||||||
24 * time.Hour, // 1 day (strict)
|
|
||||||
7 * 24 * time.Hour, // 1 week
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, duration := range testDurations {
|
|
||||||
t.Run(duration.String(), func(t *testing.T) {
|
|
||||||
netdb := NewLocalNetDb(tempDir, duration)
|
|
||||||
|
|
||||||
if netdb.MaxRouterInfoAge != duration {
|
|
||||||
t.Errorf("Expected MaxRouterInfoAge %v, got %v", duration, netdb.MaxRouterInfoAge)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for Bug #2: Race Condition in SU3 Cache Access
|
|
||||||
func TestSU3CacheRaceCondition(t *testing.T) {
|
|
||||||
// Create a mock netdb that will fail during RouterInfos() call
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_test_race")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Create a minimal netdb with no router files (this will cause rebuild to fail)
|
|
||||||
netdb := NewLocalNetDb(tempDir, 72*time.Hour)
|
|
||||||
reseeder := NewReseeder(netdb)
|
|
||||||
|
|
||||||
// Mock peer for testing
|
|
||||||
peer := Peer("testpeer")
|
|
||||||
|
|
||||||
// Test 1: Empty cache (should return 404, not panic)
|
|
||||||
_, err = reseeder.PeerSu3Bytes(peer)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error when cache is empty, got nil")
|
|
||||||
} else if err.Error() != "404" {
|
|
||||||
t.Logf("Got expected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Simulate the actual race condition where atomic.Value
|
|
||||||
// might briefly hold an empty slice during rebuild
|
|
||||||
// Force an empty slice into the cache to simulate the race
|
|
||||||
reseeder.su3s.Store([][]byte{})
|
|
||||||
|
|
||||||
// This should also return 404, not panic
|
|
||||||
_, err = reseeder.PeerSu3Bytes(peer)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error when cache is forcibly emptied, got nil")
|
|
||||||
} else if err.Error() != "404" {
|
|
||||||
t.Logf("Got expected error for empty cache: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 3: The race condition might also be about concurrent access
|
|
||||||
// Let's test if we can make it panic with specific timing
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
// Simulate rapid cache updates that might leave empty slices briefly
|
|
||||||
go func() {
|
|
||||||
reseeder.su3s.Store([][]byte{})
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
_, _ = reseeder.PeerSu3Bytes(peer)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("Race condition test completed - if we reach here, no panic occurred")
|
|
||||||
|
|
||||||
// Test 4: Additional bounds checking (the actual fix)
|
|
||||||
// Verify our bounds check works even in edge cases
|
|
||||||
testSlice := [][]byte{
|
|
||||||
[]byte("su3-file-1"),
|
|
||||||
[]byte("su3-file-2"),
|
|
||||||
}
|
|
||||||
reseeder.su3s.Store(testSlice)
|
|
||||||
|
|
||||||
// This should work normally
|
|
||||||
result, err := reseeder.PeerSu3Bytes(peer)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error with valid cache: %v", err)
|
|
||||||
}
|
|
||||||
if result == nil {
|
|
||||||
t.Error("Expected su3 bytes, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for Bug #2 Fix: Improved bounds checking in SU3 cache access
|
|
||||||
func TestSU3BoundsCheckingFix(t *testing.T) {
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_test_bounds")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
netdb := NewLocalNetDb(tempDir, 72*time.Hour)
|
|
||||||
reseeder := NewReseeder(netdb)
|
|
||||||
peer := Peer("testpeer")
|
|
||||||
|
|
||||||
// Test with valid non-empty cache
|
|
||||||
validCache := [][]byte{
|
|
||||||
[]byte("su3-file-1"),
|
|
||||||
[]byte("su3-file-2"),
|
|
||||||
[]byte("su3-file-3"),
|
|
||||||
}
|
|
||||||
reseeder.su3s.Store(validCache)
|
|
||||||
|
|
||||||
// This should work correctly
|
|
||||||
result, err := reseeder.PeerSu3Bytes(peer)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error with valid cache: %v", err)
|
|
||||||
}
|
|
||||||
if result == nil {
|
|
||||||
t.Error("Expected su3 bytes, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify we get one of the expected results
|
|
||||||
found := false
|
|
||||||
for _, expected := range validCache {
|
|
||||||
if string(result) == string(expected) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Error("Result not found in expected su3 cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("Bounds checking fix verified - proper access to su3 cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for Bug #4 Fix: Verify CLI default matches I2P standard (72 hours)
|
|
||||||
func TestRouterAgeDefaultConsistency(t *testing.T) {
|
|
||||||
// This test documents that the CLI default of 72 hours is the I2P standard
|
|
||||||
// and ensures consistency between documentation and implementation
|
|
||||||
|
|
||||||
defaultAge := 72 * time.Hour
|
|
||||||
|
|
||||||
tempDir, err := os.MkdirTemp("", "netdb_test_default")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Test that when we use the documented default (72h), it works as expected
|
|
||||||
netdb := NewLocalNetDb(tempDir, defaultAge)
|
|
||||||
|
|
||||||
if netdb.MaxRouterInfoAge != defaultAge {
|
|
||||||
t.Errorf("Expected MaxRouterInfoAge to be %v (I2P standard), got %v", defaultAge, netdb.MaxRouterInfoAge)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify this matches what the CLI flag shows as default
|
|
||||||
expectedDefault := 72 * time.Hour
|
|
||||||
if netdb.MaxRouterInfoAge != expectedDefault {
|
|
||||||
t.Errorf("Router age default inconsistency: expected %v (CLI default), got %v", expectedDefault, netdb.MaxRouterInfoAge)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Router age default correctly set to %v (I2P standard)", netdb.MaxRouterInfoAge)
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
// SharedUtilities provides common utility functions used across the reseed package.
|
|
||||||
// Moved from: various files
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AllReseeds contains the comprehensive list of known I2P reseed server URLs.
|
|
||||||
// These servers provide bootstrap router information for new I2P nodes to join the network.
|
|
||||||
// The list is used for ping testing and fallback reseed operations when needed.
|
|
||||||
var AllReseeds = []string{
|
|
||||||
"https://banana.incognet.io/",
|
|
||||||
"https://i2p.novg.net/",
|
|
||||||
"https://i2pseed.creativecowpat.net:8443/",
|
|
||||||
"https://reseed-fr.i2pd.xyz/",
|
|
||||||
"https://reseed-pl.i2pd.xyz/",
|
|
||||||
"https://reseed.diva.exchange/",
|
|
||||||
"https://reseed.i2pgit.org/",
|
|
||||||
"https://reseed.memcpy.io/",
|
|
||||||
"https://reseed.onion.im/",
|
|
||||||
"https://reseed2.i2p.net/",
|
|
||||||
"https://www2.mk16.de/",
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignerFilenameFromID converts a signer ID into a filesystem-safe filename.
|
|
||||||
// Replaces '@' symbols with '_at_' to create valid filenames for certificate storage.
|
|
||||||
// This ensures consistent file naming across different operating systems and filesystems.
|
|
||||||
func SignerFilenameFromID(signerID string) string {
|
|
||||||
return strings.Replace(signerID, "@", "_at_", 1)
|
|
||||||
}
|
|
@@ -1,138 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test for Bug #6: User Agent String Mismatch with I2P Compatibility
|
|
||||||
// This test verifies that the server strictly enforces the exact I2P user agent
|
|
||||||
// Only "Wget/1.11.4" is allowed - no other versions or variations
|
|
||||||
func TestUserAgentCompatibility(t *testing.T) {
|
|
||||||
// Create a simple handler that just returns OK
|
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("OK"))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wrap with our verification middleware
|
|
||||||
handler := verifyMiddleware(testHandler)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
userAgent string
|
|
||||||
expectedStatus int
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Exact match (current behavior)",
|
|
||||||
userAgent: "Wget/1.11.4",
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
description: "Should accept the exact expected user agent",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Newer wget version",
|
|
||||||
userAgent: "Wget/1.12.0",
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
description: "Should reject newer wget versions - only exact I2P standard allowed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Much newer wget version",
|
|
||||||
userAgent: "Wget/1.20.3",
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
description: "Should reject much newer wget versions - only exact I2P standard allowed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid user agent (not wget)",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
description: "Should reject non-wget user agents",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid user agent (curl)",
|
|
||||||
userAgent: "curl/7.68.0",
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
description: "Should reject curl and other non-wget agents",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Malformed wget version",
|
|
||||||
userAgent: "Wget/invalid",
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
description: "Should reject malformed wget versions",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty user agent",
|
|
||||||
userAgent: "",
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
description: "Should reject empty user agent",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/i2pseeds.su3", nil)
|
|
||||||
req.Header.Set("User-Agent", tc.userAgent)
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if rr.Code != tc.expectedStatus {
|
|
||||||
t.Errorf("Expected status %d, got %d for user agent %q. %s",
|
|
||||||
tc.expectedStatus, rr.Code, tc.userAgent, tc.description)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the current behavior for visibility
|
|
||||||
if tc.expectedStatus == http.StatusForbidden && rr.Code == http.StatusForbidden {
|
|
||||||
t.Logf("BLOCKED (as expected): %s -> %d", tc.userAgent, rr.Code)
|
|
||||||
} else if tc.expectedStatus == http.StatusOK && rr.Code == http.StatusOK {
|
|
||||||
t.Logf("ALLOWED (as expected): %s -> %d", tc.userAgent, rr.Code)
|
|
||||||
} else {
|
|
||||||
t.Logf("MISMATCH: %s -> %d (expected %d)", tc.userAgent, rr.Code, tc.expectedStatus)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidI2PUserAgent validates if a user agent string is the exact I2P-required user agent
|
|
||||||
// According to I2P protocol specification, ONLY "Wget/1.11.4" is valid for SU3 bundle fetching
|
|
||||||
func isValidI2PUserAgent(userAgent string) bool {
|
|
||||||
// I2P protocol requires exactly "Wget/1.11.4" - no other versions or variations allowed
|
|
||||||
return userAgent == I2pUserAgent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for the strict user agent validation (I2P protocol requirement)
|
|
||||||
func TestStrictUserAgentValidation(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
userAgent string
|
|
||||||
shouldAccept bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{"Wget/1.11.4", true, "Only valid I2P user agent"},
|
|
||||||
{"Wget/1.12.0", false, "Newer version not allowed"},
|
|
||||||
{"Wget/1.20.3", false, "Much newer version not allowed"},
|
|
||||||
{"Wget/2.0.0", false, "Major version upgrade not allowed"},
|
|
||||||
{"wget/1.11.4", false, "Lowercase wget (case sensitive)"},
|
|
||||||
{"Wget/1.11", false, "Missing patch version"},
|
|
||||||
{"Wget/1.11.4.5", false, "Too many version parts"},
|
|
||||||
{"Wget/1.11.4-ubuntu", false, "Version with suffix"},
|
|
||||||
{"Wget/abc", false, "Non-numeric version"},
|
|
||||||
{"Mozilla/5.0", false, "Browser user agent"},
|
|
||||||
{"curl/7.68.0", false, "Curl user agent"},
|
|
||||||
{"", false, "Empty user agent"},
|
|
||||||
{"Wget", false, "No version"},
|
|
||||||
{"Wget/", false, "Empty version"},
|
|
||||||
{"Wget/1.11.3", false, "Older version not allowed"},
|
|
||||||
{"Wget/1.10.4", false, "Older minor version not allowed"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.userAgent, func(t *testing.T) {
|
|
||||||
result := isValidI2PUserAgent(tc.userAgent)
|
|
||||||
if result != tc.shouldAccept {
|
|
||||||
t.Errorf("For user agent %q: expected %v, got %v. %s",
|
|
||||||
tc.userAgent, tc.shouldAccept, result, tc.description)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,31 +5,46 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"io/ioutil"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyStore struct and methods moved to keystore.go
|
type KeyStore struct {
|
||||||
|
Path string
|
||||||
// SignerFilename generates a certificate filename from a signer ID string.
|
}
|
||||||
// Appends ".crt" extension to the processed signer ID for consistent certificate file naming.
|
|
||||||
// Uses SignerFilenameFromID for consistent ID processing across the reseed system.
|
func (ks *KeyStore) ReseederCertificate(signer []byte) (*x509.Certificate, error) {
|
||||||
func SignerFilename(signer string) string {
|
return ks.reseederCertificate("reseed", signer)
|
||||||
return SignerFilenameFromID(signer) + ".crt"
|
}
|
||||||
|
|
||||||
|
func (ks *KeyStore) DirReseederCertificate(dir string, signer []byte) (*x509.Certificate, error) {
|
||||||
|
return ks.reseederCertificate(dir, signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ks *KeyStore) reseederCertificate(dir string, signer []byte) (*x509.Certificate, error) {
|
||||||
|
certFile := filepath.Base(SignerFilename(string(signer)))
|
||||||
|
certString, err := ioutil.ReadFile(filepath.Join(ks.Path, dir, certFile))
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPem, _ := pem.Decode(certString)
|
||||||
|
return x509.ParseCertificate(certPem.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignerFilename(signer string) string {
|
||||||
|
return strings.Replace(signer, "@", "_at_", 1) + ".crt"
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTLSCertificate creates a new TLS certificate for the specified hostname.
|
|
||||||
// This is a convenience wrapper around NewTLSCertificateAltNames for single-host certificates.
|
|
||||||
// Returns the certificate in PEM format ready for use in TLS server configuration.
|
|
||||||
func NewTLSCertificate(host string, priv *ecdsa.PrivateKey) ([]byte, error) {
|
func NewTLSCertificate(host string, priv *ecdsa.PrivateKey) ([]byte, error) {
|
||||||
return NewTLSCertificateAltNames(priv, host)
|
return NewTLSCertificateAltNames(priv, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTLSCertificateAltNames creates a new TLS certificate supporting multiple hostnames.
|
|
||||||
// Generates a 5-year validity certificate with specified hostnames as Subject Alternative Names
|
|
||||||
// for flexible deployment across multiple domains. Uses ECDSA private key for modern cryptography.
|
|
||||||
func NewTLSCertificateAltNames(priv *ecdsa.PrivateKey, hosts ...string) ([]byte, error) {
|
func NewTLSCertificateAltNames(priv *ecdsa.PrivateKey, hosts ...string) ([]byte, error) {
|
||||||
notBefore := time.Now()
|
notBefore := time.Now()
|
||||||
notAfter := notBefore.Add(5 * 365 * 24 * time.Hour)
|
notAfter := notBefore.Add(5 * 365 * 24 * time.Hour)
|
||||||
@@ -41,7 +56,6 @@ func NewTLSCertificateAltNames(priv *ecdsa.PrivateKey, hosts ...string) ([]byte,
|
|||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).Error("Failed to generate serial number for TLS certificate")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +91,6 @@ func NewTLSCertificateAltNames(priv *ecdsa.PrivateKey, hosts ...string) ([]byte,
|
|||||||
|
|
||||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).WithField("hosts", hosts).Error("Failed to create TLS certificate")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,526 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSignerFilename(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
signer string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Simple email address",
|
|
||||||
signer: "test@example.com",
|
|
||||||
expected: "test_at_example.com.crt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "I2P email address",
|
|
||||||
signer: "user@mail.i2p",
|
|
||||||
expected: "user_at_mail.i2p.crt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Complex email with dots",
|
|
||||||
signer: "test.user@sub.domain.com",
|
|
||||||
expected: "test.user_at_sub.domain.com.crt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Email with numbers",
|
|
||||||
signer: "user123@example456.org",
|
|
||||||
expected: "user123_at_example456.org.crt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty string",
|
|
||||||
signer: "",
|
|
||||||
expected: ".crt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "String without @ symbol",
|
|
||||||
signer: "no-at-symbol",
|
|
||||||
expected: "no-at-symbol.crt",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := SignerFilename(tt.signer)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("SignerFilename(%q) = %q, want %q", tt.signer, result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTLSCertificate(t *testing.T) {
|
|
||||||
// Generate a test private key
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
host string
|
|
||||||
wantErr bool
|
|
||||||
checkCN bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid hostname",
|
|
||||||
host: "example.com",
|
|
||||||
wantErr: false,
|
|
||||||
checkCN: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid IP address",
|
|
||||||
host: "192.168.1.1",
|
|
||||||
wantErr: false,
|
|
||||||
checkCN: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Localhost",
|
|
||||||
host: "localhost",
|
|
||||||
wantErr: false,
|
|
||||||
checkCN: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty host",
|
|
||||||
host: "",
|
|
||||||
wantErr: false,
|
|
||||||
checkCN: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
certBytes, err := NewTLSCertificate(tt.host, priv)
|
|
||||||
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("NewTLSCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tt.wantErr {
|
|
||||||
// Parse the certificate to verify it's valid
|
|
||||||
cert, err := x509.ParseCertificate(certBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to parse generated certificate: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify certificate properties
|
|
||||||
if tt.checkCN && cert.Subject.CommonName != tt.host {
|
|
||||||
t.Errorf("Certificate CommonName = %q, want %q", cert.Subject.CommonName, tt.host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a valid CA certificate
|
|
||||||
if !cert.IsCA {
|
|
||||||
t.Error("Certificate should be marked as CA")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check key usage
|
|
||||||
expectedKeyUsage := x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature
|
|
||||||
if cert.KeyUsage != expectedKeyUsage {
|
|
||||||
t.Errorf("Certificate KeyUsage = %v, want %v", cert.KeyUsage, expectedKeyUsage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check validity period (should be 5 years)
|
|
||||||
validityDuration := cert.NotAfter.Sub(cert.NotBefore)
|
|
||||||
expectedDuration := 5 * 365 * 24 * time.Hour
|
|
||||||
tolerance := 24 * time.Hour // Allow 1 day tolerance
|
|
||||||
|
|
||||||
if validityDuration < expectedDuration-tolerance || validityDuration > expectedDuration+tolerance {
|
|
||||||
t.Errorf("Certificate validity duration = %v, want approximately %v", validityDuration, expectedDuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTLSCertificateAltNames_SingleHost(t *testing.T) {
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
host := "test.example.com"
|
|
||||||
certBytes, err := NewTLSCertificateAltNames(priv, host)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewTLSCertificateAltNames() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert.Subject.CommonName != host {
|
|
||||||
t.Errorf("CommonName = %q, want %q", cert.Subject.CommonName, host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have the host in DNS names (since it gets added after splitting)
|
|
||||||
found := false
|
|
||||||
for _, dnsName := range cert.DNSNames {
|
|
||||||
if dnsName == host {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("DNS names %v should contain %q", cert.DNSNames, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTLSCertificateAltNames_MultipleHosts(t *testing.T) {
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hosts := []string{"primary.example.com", "alt1.example.com", "alt2.example.com"}
|
|
||||||
certBytes, err := NewTLSCertificateAltNames(priv, hosts...)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewTLSCertificateAltNames() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primary host should be the CommonName
|
|
||||||
if cert.Subject.CommonName != hosts[0] {
|
|
||||||
t.Errorf("CommonName = %q, want %q", cert.Subject.CommonName, hosts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// All hosts should be in DNS names
|
|
||||||
for _, expectedHost := range hosts {
|
|
||||||
found := false
|
|
||||||
for _, dnsName := range cert.DNSNames {
|
|
||||||
if dnsName == expectedHost {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("DNS names %v should contain %q", cert.DNSNames, expectedHost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTLSCertificateAltNames_IPAddresses(t *testing.T) {
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with comma-separated IPs and hostnames
|
|
||||||
hostString := "192.168.1.1,example.com,10.0.0.1"
|
|
||||||
certBytes, err := NewTLSCertificateAltNames(priv, hostString)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewTLSCertificateAltNames() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check IP addresses
|
|
||||||
expectedIPs := []string{"192.168.1.1", "10.0.0.1"}
|
|
||||||
for _, expectedIP := range expectedIPs {
|
|
||||||
ip := net.ParseIP(expectedIP)
|
|
||||||
found := false
|
|
||||||
for _, certIP := range cert.IPAddresses {
|
|
||||||
if certIP.Equal(ip) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("IP addresses %v should contain %s", cert.IPAddresses, expectedIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check DNS name
|
|
||||||
found := false
|
|
||||||
for _, dnsName := range cert.DNSNames {
|
|
||||||
if dnsName == "example.com" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("DNS names %v should contain 'example.com'", cert.DNSNames)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTLSCertificateAltNames_EmptyHosts(t *testing.T) {
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with empty slice - this should panic due to hosts[1:] access
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("Expected panic when calling with no hosts, but didn't panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, _ = NewTLSCertificateAltNames(priv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTLSCertificateAltNames_EmptyStringHost(t *testing.T) {
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with single empty string - this should work
|
|
||||||
certBytes, err := NewTLSCertificateAltNames(priv, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewTLSCertificateAltNames() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert.Subject.CommonName != "" {
|
|
||||||
t.Errorf("CommonName = %q, want empty string", cert.Subject.CommonName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyStore_ReseederCertificate(t *testing.T) {
|
|
||||||
// Create temporary directory structure
|
|
||||||
tmpDir, err := os.MkdirTemp("", "keystore_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
// Create test certificate file
|
|
||||||
signer := "test@example.com"
|
|
||||||
certFileName := SignerFilename(signer)
|
|
||||||
reseedDir := filepath.Join(tmpDir, "reseed")
|
|
||||||
err = os.MkdirAll(reseedDir, 0o755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create reseed dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a test certificate
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certBytes, err := NewTLSCertificate("test.example.com", priv)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write certificate to file
|
|
||||||
certFile := filepath.Join(reseedDir, certFileName)
|
|
||||||
pemBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
|
|
||||||
pemBytes := pem.EncodeToMemory(pemBlock)
|
|
||||||
err = os.WriteFile(certFile, pemBytes, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to write certificate file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test KeyStore
|
|
||||||
ks := &KeyStore{Path: tmpDir}
|
|
||||||
cert, err := ks.ReseederCertificate([]byte(signer))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("ReseederCertificate() error = %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert == nil {
|
|
||||||
t.Error("Expected certificate, got nil")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it's the same certificate
|
|
||||||
if cert.Subject.CommonName != "test.example.com" {
|
|
||||||
t.Errorf("Certificate CommonName = %q, want %q", cert.Subject.CommonName, "test.example.com")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyStore_ReseederCertificate_FileNotFound(t *testing.T) {
|
|
||||||
// Create temporary directory
|
|
||||||
tmpDir, err := os.MkdirTemp("", "keystore_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
ks := &KeyStore{Path: tmpDir}
|
|
||||||
signer := "nonexistent@example.com"
|
|
||||||
|
|
||||||
_, err = ks.ReseederCertificate([]byte(signer))
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for non-existent certificate, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyStore_DirReseederCertificate(t *testing.T) {
|
|
||||||
// Create temporary directory structure
|
|
||||||
tmpDir, err := os.MkdirTemp("", "keystore_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
// Create custom directory and test certificate
|
|
||||||
customDir := "custom_certs"
|
|
||||||
signer := "test@example.com"
|
|
||||||
certFileName := SignerFilename(signer)
|
|
||||||
certDir := filepath.Join(tmpDir, customDir)
|
|
||||||
err = os.MkdirAll(certDir, 0o755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create cert dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate and write test certificate
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certBytes, err := NewTLSCertificate("custom.example.com", priv)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certFile := filepath.Join(certDir, certFileName)
|
|
||||||
pemBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
|
|
||||||
pemBytes := pem.EncodeToMemory(pemBlock)
|
|
||||||
err = os.WriteFile(certFile, pemBytes, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to write certificate file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test DirReseederCertificate
|
|
||||||
ks := &KeyStore{Path: tmpDir}
|
|
||||||
cert, err := ks.DirReseederCertificate(customDir, []byte(signer))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("DirReseederCertificate() error = %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert == nil {
|
|
||||||
t.Error("Expected certificate, got nil")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert.Subject.CommonName != "custom.example.com" {
|
|
||||||
t.Errorf("Certificate CommonName = %q, want %q", cert.Subject.CommonName, "custom.example.com")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyStore_ReseederCertificate_InvalidPEM(t *testing.T) {
|
|
||||||
// Create temporary directory and invalid certificate file
|
|
||||||
tmpDir, err := os.MkdirTemp("", "keystore_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
signer := "test@example.com"
|
|
||||||
certFileName := SignerFilename(signer)
|
|
||||||
reseedDir := filepath.Join(tmpDir, "reseed")
|
|
||||||
err = os.MkdirAll(reseedDir, 0o755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create reseed dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write invalid certificate data in valid PEM format but with bad certificate bytes
|
|
||||||
// This is valid base64 but invalid certificate data
|
|
||||||
invalidPEM := `-----BEGIN CERTIFICATE-----
|
|
||||||
aW52YWxpZGNlcnRpZmljYXRlZGF0YQ==
|
|
||||||
-----END CERTIFICATE-----`
|
|
||||||
|
|
||||||
certFile := filepath.Join(reseedDir, certFileName)
|
|
||||||
err = os.WriteFile(certFile, []byte(invalidPEM), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to write invalid certificate file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ks := &KeyStore{Path: tmpDir}
|
|
||||||
_, err = ks.ReseederCertificate([]byte(signer))
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for invalid certificate, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyStore_ReseederCertificate_NonPEMData(t *testing.T) {
|
|
||||||
// Create temporary directory and non-PEM file
|
|
||||||
tmpDir, err := os.MkdirTemp("", "keystore_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
signer := "test@example.com"
|
|
||||||
certFileName := SignerFilename(signer)
|
|
||||||
reseedDir := filepath.Join(tmpDir, "reseed")
|
|
||||||
err = os.MkdirAll(reseedDir, 0o755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create reseed dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write completely invalid data that can't be parsed as PEM
|
|
||||||
certFile := filepath.Join(reseedDir, certFileName)
|
|
||||||
err = os.WriteFile(certFile, []byte("completely invalid certificate data"), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to write invalid certificate file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test captures the bug in the original code where pem.Decode returns nil
|
|
||||||
// and the code tries to access certPem.Bytes without checking for nil
|
|
||||||
ks := &KeyStore{Path: tmpDir}
|
|
||||||
|
|
||||||
// The function should panic due to nil pointer dereference
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("Expected panic due to nil pointer dereference, but didn't panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, _ = ks.ReseederCertificate([]byte(signer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark tests for performance validation
|
|
||||||
func BenchmarkSignerFilename(b *testing.B) {
|
|
||||||
signer := "benchmark@example.com"
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = SignerFilename(signer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkNewTLSCertificate(b *testing.B) {
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to generate test private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, err := NewTLSCertificate("benchmark.example.com", priv)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("NewTLSCertificate failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +1,3 @@
|
|||||||
package reseed
|
package reseed
|
||||||
|
|
||||||
// Version constant moved to constants.go
|
const Version = "0.3.4"
|
||||||
|
@@ -1,45 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GitHubRelease struct {
|
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionActuallyChanged(t *testing.T) {
|
|
||||||
// First, use the github API to get the latest github release
|
|
||||||
resp, err := http.Get("https://api.github.com/repos/go-i2p/reseed-tools/releases/latest")
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("Failed to fetch GitHub release: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var release GitHubRelease
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
|
||||||
t.Skipf("Failed to decode GitHub response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
githubVersion := release.TagName
|
|
||||||
if githubVersion == "" {
|
|
||||||
t.Skip("No GitHub release found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 'v' prefix if present
|
|
||||||
if len(githubVersion) > 0 && githubVersion[0] == 'v' {
|
|
||||||
githubVersion = githubVersion[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, compare it to the current version
|
|
||||||
if Version == githubVersion {
|
|
||||||
t.Fatal("Version not updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the current version is larger than the previous version
|
|
||||||
if Version < githubVersion {
|
|
||||||
t.Fatalf("Version not incremented: current %s < github %s", Version, githubVersion)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -3,7 +3,7 @@ package reseed
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func zipSeeds(seeds []routerInfo) ([]byte, error) {
|
func zipSeeds(seeds []routerInfo) ([]byte, error) {
|
||||||
@@ -19,19 +19,16 @@ func zipSeeds(seeds []routerInfo) ([]byte, error) {
|
|||||||
fileHeader.SetModTime(file.ModTime)
|
fileHeader.SetModTime(file.ModTime)
|
||||||
zipFile, err := zipWriter.CreateHeader(fileHeader)
|
zipFile, err := zipWriter.CreateHeader(fileHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).WithField("file_name", file.Name).Error("Failed to create zip file header")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = zipFile.Write(file.Data)
|
_, err = zipFile.Write(file.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).WithField("file_name", file.Name).Error("Failed to write file data to zip")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := zipWriter.Close(); err != nil {
|
if err := zipWriter.Close(); err != nil {
|
||||||
lgr.WithError(err).Error("Failed to close zip writer")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +39,6 @@ func uzipSeeds(c []byte) ([]routerInfo, error) {
|
|||||||
input := bytes.NewReader(c)
|
input := bytes.NewReader(c)
|
||||||
zipReader, err := zip.NewReader(input, int64(len(c)))
|
zipReader, err := zip.NewReader(input, int64(len(c)))
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).WithField("zip_size", len(c)).Error("Failed to create zip reader")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,13 +46,11 @@ func uzipSeeds(c []byte) ([]routerInfo, error) {
|
|||||||
for _, f := range zipReader.File {
|
for _, f := range zipReader.File {
|
||||||
rc, err := f.Open()
|
rc, err := f.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lgr.WithError(err).WithField("file_name", f.Name).Error("Failed to open file from zip")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(rc)
|
data, err := ioutil.ReadAll(rc)
|
||||||
rc.Close()
|
rc.Close()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).WithField("file_name", f.Name).Error("Failed to read file data from zip")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,392 +0,0 @@
|
|||||||
package reseed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestZipSeeds_Success(t *testing.T) {
|
|
||||||
// Test with valid router info data
|
|
||||||
testTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
||||||
seeds := []routerInfo{
|
|
||||||
{
|
|
||||||
Name: "routerInfo-test1.dat",
|
|
||||||
ModTime: testTime,
|
|
||||||
Data: []byte("test router info data 1"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "routerInfo-test2.dat",
|
|
||||||
ModTime: testTime,
|
|
||||||
Data: []byte("test router info data 2"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
zipData, err := zipSeeds(seeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("zipSeeds() error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(zipData) == 0 {
|
|
||||||
t.Error("zipSeeds() returned empty data")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the zip file structure
|
|
||||||
reader := bytes.NewReader(zipData)
|
|
||||||
zipReader, err := zip.NewReader(reader, int64(len(zipData)))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read zip data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(zipReader.File) != 2 {
|
|
||||||
t.Errorf("Expected 2 files in zip, got %d", len(zipReader.File))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify file names and content
|
|
||||||
expectedFiles := map[string]string{
|
|
||||||
"routerInfo-test1.dat": "test router info data 1",
|
|
||||||
"routerInfo-test2.dat": "test router info data 2",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range zipReader.File {
|
|
||||||
expectedContent, exists := expectedFiles[file.Name]
|
|
||||||
if !exists {
|
|
||||||
t.Errorf("Unexpected file in zip: %s", file.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check modification time
|
|
||||||
if !file.ModTime().Equal(testTime) {
|
|
||||||
t.Errorf("File %s has wrong ModTime. Expected %v, got %v", file.Name, testTime, file.ModTime())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check compression method
|
|
||||||
if file.Method != zip.Deflate {
|
|
||||||
t.Errorf("File %s has wrong compression method. Expected %d, got %d", file.Name, zip.Deflate, file.Method)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check content
|
|
||||||
rc, err := file.Open()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to open file %s: %v", file.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var content bytes.Buffer
|
|
||||||
_, err = content.ReadFrom(rc)
|
|
||||||
rc.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to read file %s: %v", file.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if content.String() != expectedContent {
|
|
||||||
t.Errorf("File %s has wrong content. Expected %q, got %q", file.Name, expectedContent, content.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZipSeeds_EmptyInput(t *testing.T) {
|
|
||||||
// Test with empty slice
|
|
||||||
seeds := []routerInfo{}
|
|
||||||
|
|
||||||
zipData, err := zipSeeds(seeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("zipSeeds() error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it creates a valid but empty zip file
|
|
||||||
reader := bytes.NewReader(zipData)
|
|
||||||
zipReader, err := zip.NewReader(reader, int64(len(zipData)))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read empty zip data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(zipReader.File) != 0 {
|
|
||||||
t.Errorf("Expected 0 files in empty zip, got %d", len(zipReader.File))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZipSeeds_SingleFile(t *testing.T) {
|
|
||||||
// Test with single router info
|
|
||||||
testTime := time.Date(2023, 6, 15, 10, 30, 0, 0, time.UTC)
|
|
||||||
seeds := []routerInfo{
|
|
||||||
{
|
|
||||||
Name: "single-router.dat",
|
|
||||||
ModTime: testTime,
|
|
||||||
Data: []byte("single router data"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
zipData, err := zipSeeds(seeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("zipSeeds() error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := bytes.NewReader(zipData)
|
|
||||||
zipReader, err := zip.NewReader(reader, int64(len(zipData)))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read zip data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(zipReader.File) != 1 {
|
|
||||||
t.Errorf("Expected 1 file in zip, got %d", len(zipReader.File))
|
|
||||||
}
|
|
||||||
|
|
||||||
file := zipReader.File[0]
|
|
||||||
if file.Name != "single-router.dat" {
|
|
||||||
t.Errorf("Expected file name 'single-router.dat', got %q", file.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUzipSeeds_Success(t *testing.T) {
|
|
||||||
// First create a zip file using zipSeeds
|
|
||||||
testTime := time.Date(2024, 2, 14, 8, 45, 0, 0, time.UTC)
|
|
||||||
originalSeeds := []routerInfo{
|
|
||||||
{
|
|
||||||
Name: "router1.dat",
|
|
||||||
ModTime: testTime,
|
|
||||||
Data: []byte("router 1 content"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "router2.dat",
|
|
||||||
ModTime: testTime,
|
|
||||||
Data: []byte("router 2 content"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
zipData, err := zipSeeds(originalSeeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Setup failed: zipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now test uzipSeeds
|
|
||||||
unzippedSeeds, err := uzipSeeds(zipData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("uzipSeeds() error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(unzippedSeeds) != 2 {
|
|
||||||
t.Errorf("Expected 2 seeds, got %d", len(unzippedSeeds))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a map for easier comparison
|
|
||||||
seedMap := make(map[string]routerInfo)
|
|
||||||
for _, seed := range unzippedSeeds {
|
|
||||||
seedMap[seed.Name] = seed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check first file
|
|
||||||
if seed1, exists := seedMap["router1.dat"]; exists {
|
|
||||||
if string(seed1.Data) != "router 1 content" {
|
|
||||||
t.Errorf("router1.dat content mismatch. Expected %q, got %q", "router 1 content", string(seed1.Data))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Error("router1.dat not found in unzipped seeds")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check second file
|
|
||||||
if seed2, exists := seedMap["router2.dat"]; exists {
|
|
||||||
if string(seed2.Data) != "router 2 content" {
|
|
||||||
t.Errorf("router2.dat content mismatch. Expected %q, got %q", "router 2 content", string(seed2.Data))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Error("router2.dat not found in unzipped seeds")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUzipSeeds_EmptyZip(t *testing.T) {
|
|
||||||
// Create an empty zip file
|
|
||||||
emptySeeds := []routerInfo{}
|
|
||||||
zipData, err := zipSeeds(emptySeeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Setup failed: zipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unzippedSeeds, err := uzipSeeds(zipData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("uzipSeeds() error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(unzippedSeeds) != 0 {
|
|
||||||
t.Errorf("Expected 0 seeds from empty zip, got %d", len(unzippedSeeds))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUzipSeeds_InvalidZipData(t *testing.T) {
|
|
||||||
// Test with invalid zip data
|
|
||||||
invalidData := []byte("this is not a zip file")
|
|
||||||
|
|
||||||
unzippedSeeds, err := uzipSeeds(invalidData)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("uzipSeeds() should return error for invalid zip data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if unzippedSeeds != nil {
|
|
||||||
t.Error("uzipSeeds() should return nil seeds for invalid zip data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUzipSeeds_EmptyData(t *testing.T) {
|
|
||||||
// Test with empty byte slice
|
|
||||||
emptyData := []byte{}
|
|
||||||
|
|
||||||
unzippedSeeds, err := uzipSeeds(emptyData)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("uzipSeeds() should return error for empty data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if unzippedSeeds != nil {
|
|
||||||
t.Error("uzipSeeds() should return nil seeds for empty data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZipUnzipRoundTrip(t *testing.T) {
|
|
||||||
// Test round-trip: zip -> unzip -> compare
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
seeds []routerInfo
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "MultipleFiles",
|
|
||||||
seeds: []routerInfo{
|
|
||||||
{Name: "file1.dat", ModTime: time.Now(), Data: []byte("data1")},
|
|
||||||
{Name: "file2.dat", ModTime: time.Now(), Data: []byte("data2")},
|
|
||||||
{Name: "file3.dat", ModTime: time.Now(), Data: []byte("data3")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SingleFile",
|
|
||||||
seeds: []routerInfo{
|
|
||||||
{Name: "single.dat", ModTime: time.Now(), Data: []byte("single data")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty",
|
|
||||||
seeds: []routerInfo{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "LargeData",
|
|
||||||
seeds: []routerInfo{
|
|
||||||
{Name: "large.dat", ModTime: time.Now(), Data: bytes.Repeat([]byte("x"), 10000)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Zip the seeds
|
|
||||||
zipData, err := zipSeeds(tt.seeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("zipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unzip the data
|
|
||||||
unzippedSeeds, err := uzipSeeds(zipData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("uzipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare lengths
|
|
||||||
if len(unzippedSeeds) != len(tt.seeds) {
|
|
||||||
t.Errorf("Length mismatch: original=%d, unzipped=%d", len(tt.seeds), len(unzippedSeeds))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create maps for comparison (order might be different)
|
|
||||||
originalMap := make(map[string][]byte)
|
|
||||||
for _, seed := range tt.seeds {
|
|
||||||
originalMap[seed.Name] = seed.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
unzippedMap := make(map[string][]byte)
|
|
||||||
for _, seed := range unzippedSeeds {
|
|
||||||
unzippedMap[seed.Name] = seed.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(originalMap, unzippedMap) {
|
|
||||||
t.Errorf("Round-trip failed: data mismatch")
|
|
||||||
t.Logf("Original: %v", originalMap)
|
|
||||||
t.Logf("Unzipped: %v", unzippedMap)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZipSeeds_BinaryData(t *testing.T) {
|
|
||||||
// Test with binary data (not just text)
|
|
||||||
binaryData := make([]byte, 256)
|
|
||||||
for i := range binaryData {
|
|
||||||
binaryData[i] = byte(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
seeds := []routerInfo{
|
|
||||||
{
|
|
||||||
Name: "binary.dat",
|
|
||||||
ModTime: time.Now(),
|
|
||||||
Data: binaryData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
zipData, err := zipSeeds(seeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("zipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unzippedSeeds, err := uzipSeeds(zipData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("uzipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(unzippedSeeds) != 1 {
|
|
||||||
t.Fatalf("Expected 1 unzipped seed, got %d", len(unzippedSeeds))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(unzippedSeeds[0].Data, binaryData) {
|
|
||||||
t.Error("Binary data corrupted during zip/unzip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZipSeeds_SpecialCharactersInFilename(t *testing.T) {
|
|
||||||
// Test with filenames containing special characters
|
|
||||||
seeds := []routerInfo{
|
|
||||||
{
|
|
||||||
Name: "file-with-dashes.dat",
|
|
||||||
ModTime: time.Now(),
|
|
||||||
Data: []byte("dash data"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "file_with_underscores.dat",
|
|
||||||
ModTime: time.Now(),
|
|
||||||
Data: []byte("underscore data"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
zipData, err := zipSeeds(seeds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("zipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unzippedSeeds, err := uzipSeeds(zipData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("uzipSeeds() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(unzippedSeeds) != 2 {
|
|
||||||
t.Fatalf("Expected 2 unzipped seeds, got %d", len(unzippedSeeds))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify filenames are preserved
|
|
||||||
foundFiles := make(map[string]bool)
|
|
||||||
for _, seed := range unzippedSeeds {
|
|
||||||
foundFiles[seed.Name] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundFiles["file-with-dashes.dat"] {
|
|
||||||
t.Error("File with dashes not found")
|
|
||||||
}
|
|
||||||
if !foundFiles["file_with_underscores.dat"] {
|
|
||||||
t.Error("File with underscores not found")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,93 +0,0 @@
|
|||||||
package su3
|
|
||||||
|
|
||||||
// SU3 File format constants
|
|
||||||
// Moved from: su3.go
|
|
||||||
const (
|
|
||||||
// minVersionLength specifies the minimum required length for version fields in SU3 files.
|
|
||||||
// Version fields shorter than this will be zero-padded to meet the requirement.
|
|
||||||
minVersionLength = 16
|
|
||||||
|
|
||||||
// SigTypeDSA represents DSA signature algorithm with SHA1 hash.
|
|
||||||
// This is the legacy signature type for backward compatibility.
|
|
||||||
SigTypeDSA = uint16(0)
|
|
||||||
|
|
||||||
// SigTypeECDSAWithSHA256 represents ECDSA signature algorithm with SHA256 hash.
|
|
||||||
// Provides 256-bit security level with efficient elliptic curve cryptography.
|
|
||||||
SigTypeECDSAWithSHA256 = uint16(1)
|
|
||||||
|
|
||||||
// SigTypeECDSAWithSHA384 represents ECDSA signature algorithm with SHA384 hash.
|
|
||||||
// Provides 384-bit security level for enhanced cryptographic strength.
|
|
||||||
SigTypeECDSAWithSHA384 = uint16(2)
|
|
||||||
|
|
||||||
// SigTypeECDSAWithSHA512 represents ECDSA signature algorithm with SHA512 hash.
|
|
||||||
// Provides maximum security level with 512-bit hash function.
|
|
||||||
SigTypeECDSAWithSHA512 = uint16(3)
|
|
||||||
|
|
||||||
// SigTypeRSAWithSHA256 represents RSA signature algorithm with SHA256 hash.
|
|
||||||
// Standard RSA signing with 256-bit hash, commonly used for 2048-bit keys.
|
|
||||||
SigTypeRSAWithSHA256 = uint16(4)
|
|
||||||
|
|
||||||
// SigTypeRSAWithSHA384 represents RSA signature algorithm with SHA384 hash.
|
|
||||||
// Enhanced RSA signing with 384-bit hash for stronger cryptographic assurance.
|
|
||||||
SigTypeRSAWithSHA384 = uint16(5)
|
|
||||||
|
|
||||||
// SigTypeRSAWithSHA512 represents RSA signature algorithm with SHA512 hash.
|
|
||||||
// Maximum strength RSA signing with 512-bit hash, default for new SU3 files.
|
|
||||||
SigTypeRSAWithSHA512 = uint16(6)
|
|
||||||
|
|
||||||
// ContentTypeUnknown indicates SU3 file contains unspecified content type.
|
|
||||||
// Used when the content type cannot be determined or is not categorized.
|
|
||||||
ContentTypeUnknown = uint8(0)
|
|
||||||
|
|
||||||
// ContentTypeRouter indicates SU3 file contains I2P router information.
|
|
||||||
// Typically used for distributing router updates and configurations.
|
|
||||||
ContentTypeRouter = uint8(1)
|
|
||||||
|
|
||||||
// ContentTypePlugin indicates SU3 file contains I2P plugin data.
|
|
||||||
// Used for distributing plugin packages and extensions to I2P routers.
|
|
||||||
ContentTypePlugin = uint8(2)
|
|
||||||
|
|
||||||
// ContentTypeReseed indicates SU3 file contains reseed bundle data.
|
|
||||||
// Contains bootstrap router information for new I2P nodes to join the network.
|
|
||||||
ContentTypeReseed = uint8(3)
|
|
||||||
|
|
||||||
// ContentTypeNews indicates SU3 file contains news or announcement data.
|
|
||||||
// Used for distributing network announcements and informational content.
|
|
||||||
ContentTypeNews = uint8(4)
|
|
||||||
|
|
||||||
// ContentTypeBlocklist indicates SU3 file contains blocklist information.
|
|
||||||
// Contains lists of blocked or banned router identities for network security.
|
|
||||||
ContentTypeBlocklist = uint8(5)
|
|
||||||
|
|
||||||
// FileTypeZIP indicates SU3 file content is compressed in ZIP format.
|
|
||||||
// Most common file type for distributing compressed collections of files.
|
|
||||||
FileTypeZIP = uint8(0)
|
|
||||||
|
|
||||||
// FileTypeXML indicates SU3 file content is in XML format.
|
|
||||||
// Used for structured data and configuration files.
|
|
||||||
FileTypeXML = uint8(1)
|
|
||||||
|
|
||||||
// FileTypeHTML indicates SU3 file content is in HTML format.
|
|
||||||
// Used for web content and documentation distribution.
|
|
||||||
FileTypeHTML = uint8(2)
|
|
||||||
|
|
||||||
// FileTypeXMLGZ indicates SU3 file content is gzip-compressed XML.
|
|
||||||
// Combines XML structure with gzip compression for efficient transmission.
|
|
||||||
FileTypeXMLGZ = uint8(3)
|
|
||||||
|
|
||||||
// FileTypeTXTGZ indicates SU3 file content is gzip-compressed text.
|
|
||||||
// Used for compressed text files and logs.
|
|
||||||
FileTypeTXTGZ = uint8(4)
|
|
||||||
|
|
||||||
// FileTypeDMG indicates SU3 file content is in Apple DMG format.
|
|
||||||
// Used for macOS application and software distribution.
|
|
||||||
FileTypeDMG = uint8(5)
|
|
||||||
|
|
||||||
// FileTypeEXE indicates SU3 file content is a Windows executable.
|
|
||||||
// Used for Windows application and software distribution.
|
|
||||||
FileTypeEXE = uint8(6)
|
|
||||||
|
|
||||||
// magicBytes defines the magic number identifier for SU3 file format.
|
|
||||||
// All valid SU3 files must begin with this exact byte sequence.
|
|
||||||
magicBytes = "I2Psu3"
|
|
||||||
)
|
|
@@ -10,38 +10,19 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-i2p/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var lgr = logger.GetGoI2PLogger()
|
|
||||||
|
|
||||||
// dsaSignature represents a DSA signature containing R and S components.
|
|
||||||
// Used for ASN.1 encoding/decoding of DSA signatures in SU3 verification.
|
|
||||||
type dsaSignature struct {
|
type dsaSignature struct {
|
||||||
R, S *big.Int
|
R, S *big.Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ecdsaSignature represents an ECDSA signature, which has the same structure as DSA.
|
|
||||||
// This type alias provides semantic clarity when working with ECDSA signatures.
|
|
||||||
type ecdsaSignature dsaSignature
|
type ecdsaSignature dsaSignature
|
||||||
|
|
||||||
// checkSignature verifies a digital signature against signed data using the specified certificate.
|
|
||||||
// It supports RSA, DSA, and ECDSA signature algorithms with various hash functions (SHA1, SHA256, SHA384, SHA512).
|
|
||||||
// This function extends the standard x509 signature verification to support additional algorithms needed for SU3 files.
|
|
||||||
func checkSignature(c *x509.Certificate, algo x509.SignatureAlgorithm, signed, signature []byte) (err error) {
|
func checkSignature(c *x509.Certificate, algo x509.SignatureAlgorithm, signed, signature []byte) (err error) {
|
||||||
if c == nil {
|
|
||||||
lgr.Error("Certificate is nil during signature verification")
|
|
||||||
return errors.New("x509: certificate is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
var hashType crypto.Hash
|
var hashType crypto.Hash
|
||||||
|
|
||||||
// Map signature algorithm to appropriate hash function
|
|
||||||
// Each algorithm specifies both the signature method and hash type
|
|
||||||
switch algo {
|
switch algo {
|
||||||
case x509.SHA1WithRSA, x509.DSAWithSHA1, x509.ECDSAWithSHA1:
|
case x509.SHA1WithRSA, x509.DSAWithSHA1, x509.ECDSAWithSHA1:
|
||||||
hashType = crypto.SHA1
|
hashType = crypto.SHA1
|
||||||
@@ -52,12 +33,10 @@ func checkSignature(c *x509.Certificate, algo x509.SignatureAlgorithm, signed, s
|
|||||||
case x509.SHA512WithRSA, x509.ECDSAWithSHA512:
|
case x509.SHA512WithRSA, x509.ECDSAWithSHA512:
|
||||||
hashType = crypto.SHA512
|
hashType = crypto.SHA512
|
||||||
default:
|
default:
|
||||||
lgr.WithField("algorithm", algo).Error("Unsupported signature algorithm")
|
|
||||||
return x509.ErrUnsupportedAlgorithm
|
return x509.ErrUnsupportedAlgorithm
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hashType.Available() {
|
if !hashType.Available() {
|
||||||
lgr.WithField("hash_type", hashType).Error("Hash type not available")
|
|
||||||
return x509.ErrUnsupportedAlgorithm
|
return x509.ErrUnsupportedAlgorithm
|
||||||
}
|
}
|
||||||
h := hashType.New()
|
h := hashType.New()
|
||||||
@@ -65,8 +44,6 @@ func checkSignature(c *x509.Certificate, algo x509.SignatureAlgorithm, signed, s
|
|||||||
h.Write(signed)
|
h.Write(signed)
|
||||||
digest := h.Sum(nil)
|
digest := h.Sum(nil)
|
||||||
|
|
||||||
// Verify signature based on public key algorithm type
|
|
||||||
// Each algorithm has different signature formats and verification procedures
|
|
||||||
switch pub := c.PublicKey.(type) {
|
switch pub := c.PublicKey.(type) {
|
||||||
case *rsa.PublicKey:
|
case *rsa.PublicKey:
|
||||||
// the digest is already hashed, so we force a 0 here
|
// the digest is already hashed, so we force a 0 here
|
||||||
@@ -74,46 +51,31 @@ func checkSignature(c *x509.Certificate, algo x509.SignatureAlgorithm, signed, s
|
|||||||
case *dsa.PublicKey:
|
case *dsa.PublicKey:
|
||||||
dsaSig := new(dsaSignature)
|
dsaSig := new(dsaSignature)
|
||||||
if _, err := asn1.Unmarshal(signature, dsaSig); err != nil {
|
if _, err := asn1.Unmarshal(signature, dsaSig); err != nil {
|
||||||
lgr.WithError(err).Error("Failed to unmarshal DSA signature")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Validate DSA signature components are positive integers
|
|
||||||
// Zero or negative values indicate malformed or invalid signatures
|
|
||||||
if dsaSig.R.Sign() <= 0 || dsaSig.S.Sign() <= 0 {
|
if dsaSig.R.Sign() <= 0 || dsaSig.S.Sign() <= 0 {
|
||||||
lgr.WithField("r_sign", dsaSig.R.Sign()).WithField("s_sign", dsaSig.S.Sign()).Error("DSA signature contained zero or negative values")
|
|
||||||
return errors.New("x509: DSA signature contained zero or negative values")
|
return errors.New("x509: DSA signature contained zero or negative values")
|
||||||
}
|
}
|
||||||
if !dsa.Verify(pub, digest, dsaSig.R, dsaSig.S) {
|
if !dsa.Verify(pub, digest, dsaSig.R, dsaSig.S) {
|
||||||
lgr.Error("DSA signature verification failed")
|
|
||||||
return errors.New("x509: DSA verification failure")
|
return errors.New("x509: DSA verification failure")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
ecdsaSig := new(ecdsaSignature)
|
ecdsaSig := new(ecdsaSignature)
|
||||||
if _, err := asn1.Unmarshal(signature, ecdsaSig); err != nil {
|
if _, err := asn1.Unmarshal(signature, ecdsaSig); err != nil {
|
||||||
lgr.WithError(err).Error("Failed to unmarshal ECDSA signature")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Validate ECDSA signature components are positive integers
|
|
||||||
// Similar validation to DSA as both use R,S component pairs
|
|
||||||
if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 {
|
if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 {
|
||||||
lgr.WithField("r_sign", ecdsaSig.R.Sign()).WithField("s_sign", ecdsaSig.S.Sign()).Error("ECDSA signature contained zero or negative values")
|
|
||||||
return errors.New("x509: ECDSA signature contained zero or negative values")
|
return errors.New("x509: ECDSA signature contained zero or negative values")
|
||||||
}
|
}
|
||||||
if !ecdsa.Verify(pub, digest, ecdsaSig.R, ecdsaSig.S) {
|
if !ecdsa.Verify(pub, digest, ecdsaSig.R, ecdsaSig.S) {
|
||||||
lgr.Error("ECDSA signature verification failed")
|
|
||||||
return errors.New("x509: ECDSA verification failure")
|
return errors.New("x509: ECDSA verification failure")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lgr.WithField("public_key_type", fmt.Sprintf("%T", c.PublicKey)).Error("Unsupported public key algorithm")
|
|
||||||
return x509.ErrUnsupportedAlgorithm
|
return x509.ErrUnsupportedAlgorithm
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSigningCertificate creates a self-signed X.509 certificate for SU3 file signing.
|
|
||||||
// It generates a certificate with the specified signer ID and RSA private key for use in
|
|
||||||
// I2P reseed operations. The certificate is valid for 10 years and includes proper key usage
|
|
||||||
// extensions for digital signatures.
|
|
||||||
func NewSigningCertificate(signerID string, privateKey *rsa.PrivateKey) ([]byte, error) {
|
func NewSigningCertificate(signerID string, privateKey *rsa.PrivateKey) ([]byte, error) {
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
@@ -121,22 +83,10 @@ func NewSigningCertificate(signerID string, privateKey *rsa.PrivateKey) ([]byte,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var subjectKeyId []byte
|
|
||||||
isCA := true
|
|
||||||
// Configure certificate authority status based on signer ID presence
|
|
||||||
// Empty signer IDs create non-CA certificates to prevent auto-generation issues
|
|
||||||
if signerID != "" {
|
|
||||||
subjectKeyId = []byte(signerID)
|
|
||||||
} else {
|
|
||||||
// When signerID is empty, create non-CA certificate to prevent auto-generation of SubjectKeyId
|
|
||||||
subjectKeyId = []byte("")
|
|
||||||
isCA = false
|
|
||||||
}
|
|
||||||
|
|
||||||
template := &x509.Certificate{
|
template := &x509.Certificate{
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
IsCA: isCA,
|
IsCA: true,
|
||||||
SubjectKeyId: subjectKeyId,
|
SubjectKeyId: []byte(signerID),
|
||||||
SerialNumber: serialNumber,
|
SerialNumber: serialNumber,
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{"I2P Anonymous Network"},
|
Organization: []string{"I2P Anonymous Network"},
|
||||||
@@ -154,8 +104,7 @@ func NewSigningCertificate(signerID string, privateKey *rsa.PrivateKey) ([]byte,
|
|||||||
|
|
||||||
publicKey := &privateKey.PublicKey
|
publicKey := &privateKey.PublicKey
|
||||||
|
|
||||||
// Create self-signed certificate using template as both subject and issuer
|
// create a self-signed certificate. template = parent
|
||||||
// This generates a root certificate suitable for SU3 file signing operations
|
|
||||||
parent := template
|
parent := template
|
||||||
cert, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, privateKey)
|
cert, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -1,529 +0,0 @@
|
|||||||
package su3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewSigningCertificate_ValidInput(t *testing.T) {
|
|
||||||
// Generate test RSA key
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signerID := "test@example.com"
|
|
||||||
|
|
||||||
// Test certificate creation
|
|
||||||
certDER, err := NewSigningCertificate(signerID, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewSigningCertificate failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(certDER) == 0 {
|
|
||||||
t.Fatal("Certificate should not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the certificate to verify it's valid
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse generated certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify certificate properties
|
|
||||||
if cert.Subject.CommonName != signerID {
|
|
||||||
t.Errorf("Expected CommonName %s, got %s", signerID, cert.Subject.CommonName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cert.IsCA {
|
|
||||||
t.Error("Certificate should be marked as CA")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cert.BasicConstraintsValid {
|
|
||||||
t.Error("BasicConstraintsValid should be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify organization details
|
|
||||||
expectedOrg := []string{"I2P Anonymous Network"}
|
|
||||||
if len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != expectedOrg[0] {
|
|
||||||
t.Errorf("Expected Organization %v, got %v", expectedOrg, cert.Subject.Organization)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedOU := []string{"I2P"}
|
|
||||||
if len(cert.Subject.OrganizationalUnit) == 0 || cert.Subject.OrganizationalUnit[0] != expectedOU[0] {
|
|
||||||
t.Errorf("Expected OrganizationalUnit %v, got %v", expectedOU, cert.Subject.OrganizationalUnit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify key usage
|
|
||||||
expectedKeyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign
|
|
||||||
if cert.KeyUsage != expectedKeyUsage {
|
|
||||||
t.Errorf("Expected KeyUsage %d, got %d", expectedKeyUsage, cert.KeyUsage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify extended key usage
|
|
||||||
expectedExtKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
|
|
||||||
if len(cert.ExtKeyUsage) != len(expectedExtKeyUsage) {
|
|
||||||
t.Errorf("Expected ExtKeyUsage length %d, got %d", len(expectedExtKeyUsage), len(cert.ExtKeyUsage))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify certificate validity period
|
|
||||||
now := time.Now()
|
|
||||||
if cert.NotBefore.After(now) {
|
|
||||||
t.Error("Certificate NotBefore should be before current time")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be valid for 10 years
|
|
||||||
expectedExpiry := now.AddDate(10, 0, 0)
|
|
||||||
if cert.NotAfter.Before(expectedExpiry.AddDate(0, 0, -1)) { // Allow 1 day tolerance
|
|
||||||
t.Error("Certificate should be valid for approximately 10 years")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSigningCertificate_DifferentSignerIDs(t *testing.T) {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
signerID string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Email format",
|
|
||||||
signerID: "user@domain.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "I2P domain",
|
|
||||||
signerID: "test@mail.i2p",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Simple identifier",
|
|
||||||
signerID: "testsigner",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "With spaces",
|
|
||||||
signerID: "Test Signer",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty string",
|
|
||||||
signerID: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
certDER, err := NewSigningCertificate(tc.signerID, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewSigningCertificate failed for %s: %v", tc.signerID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate for %s: %v", tc.signerID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert.Subject.CommonName != tc.signerID {
|
|
||||||
t.Errorf("Expected CommonName %s, got %s", tc.signerID, cert.Subject.CommonName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify SubjectKeyId is set to signerID bytes
|
|
||||||
if string(cert.SubjectKeyId) != tc.signerID {
|
|
||||||
t.Errorf("Expected SubjectKeyId %s, got %s", tc.signerID, string(cert.SubjectKeyId))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSigningCertificate_NilPrivateKey(t *testing.T) {
|
|
||||||
signerID := "test@example.com"
|
|
||||||
|
|
||||||
// The function should handle nil private key gracefully or panic
|
|
||||||
// Since the current implementation doesn't check for nil, we expect a panic
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("Expected panic when private key is nil, but function completed normally")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err := NewSigningCertificate(signerID, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error when private key is nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSigningCertificate_SerialNumberUniqueness(t *testing.T) {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signerID := "test@example.com"
|
|
||||||
|
|
||||||
// Generate multiple certificates
|
|
||||||
cert1DER, err := NewSigningCertificate(signerID, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create first certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert2DER, err := NewSigningCertificate(signerID, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create second certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert1, err := x509.ParseCertificate(cert1DER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse first certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert2, err := x509.ParseCertificate(cert2DER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse second certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serial numbers should be different
|
|
||||||
if cert1.SerialNumber.Cmp(cert2.SerialNumber) == 0 {
|
|
||||||
t.Error("Serial numbers should be unique across different certificate generations")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckSignature_RSASignatures(t *testing.T) {
|
|
||||||
// Generate test certificate and private key
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := NewSigningCertificate("test@example.com", privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testData := []byte("test data to sign")
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
algorithm x509.SignatureAlgorithm
|
|
||||||
shouldErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "SHA256WithRSA",
|
|
||||||
algorithm: x509.SHA256WithRSA,
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SHA384WithRSA",
|
|
||||||
algorithm: x509.SHA384WithRSA,
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SHA512WithRSA",
|
|
||||||
algorithm: x509.SHA512WithRSA,
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SHA1WithRSA",
|
|
||||||
algorithm: x509.SHA1WithRSA,
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "UnsupportedAlgorithm",
|
|
||||||
algorithm: x509.SignatureAlgorithm(999),
|
|
||||||
shouldErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
if tc.shouldErr {
|
|
||||||
// Test with dummy signature for unsupported algorithm
|
|
||||||
err := checkSignature(cert, tc.algorithm, testData, []byte("dummy"))
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for unsupported algorithm")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a proper signature for supported algorithms
|
|
||||||
// For this test, we'll create a minimal valid signature
|
|
||||||
// In a real scenario, this would be done through proper RSA signing
|
|
||||||
signature := make([]byte, 256) // Appropriate size for RSA 2048
|
|
||||||
copy(signature, []byte("test signature data"))
|
|
||||||
|
|
||||||
// Note: This will likely fail signature verification, but should not error
|
|
||||||
// on algorithm support - we're mainly testing the algorithm dispatch logic
|
|
||||||
err := checkSignature(cert, tc.algorithm, testData, signature)
|
|
||||||
// We expect a verification failure, not an algorithm error
|
|
||||||
// The important thing is that it doesn't return an "unsupported algorithm" error
|
|
||||||
if err == x509.ErrUnsupportedAlgorithm {
|
|
||||||
t.Errorf("Algorithm %v should be supported", tc.algorithm)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckSignature_InvalidInputs(t *testing.T) {
|
|
||||||
// Generate test certificate
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := NewSigningCertificate("test@example.com", privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testData := []byte("test data")
|
|
||||||
validSignature := make([]byte, 256)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
cert *x509.Certificate
|
|
||||||
algorithm x509.SignatureAlgorithm
|
|
||||||
data []byte
|
|
||||||
signature []byte
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Nil certificate",
|
|
||||||
cert: nil,
|
|
||||||
algorithm: x509.SHA256WithRSA,
|
|
||||||
data: testData,
|
|
||||||
signature: validSignature,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty data",
|
|
||||||
cert: cert,
|
|
||||||
algorithm: x509.SHA256WithRSA,
|
|
||||||
data: []byte{},
|
|
||||||
signature: validSignature,
|
|
||||||
expectErr: false, // Empty data should be hashable
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty signature",
|
|
||||||
cert: cert,
|
|
||||||
algorithm: x509.SHA256WithRSA,
|
|
||||||
data: testData,
|
|
||||||
signature: []byte{},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Nil signature",
|
|
||||||
cert: cert,
|
|
||||||
algorithm: x509.SHA256WithRSA,
|
|
||||||
data: testData,
|
|
||||||
signature: nil,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
err := checkSignature(tc.cert, tc.algorithm, tc.data, tc.signature)
|
|
||||||
|
|
||||||
if tc.expectErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error but got none")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We might get a verification error, but it shouldn't be a panic or unexpected error type
|
|
||||||
if err == x509.ErrUnsupportedAlgorithm {
|
|
||||||
t.Error("Should not get unsupported algorithm error for valid inputs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDSASignatureStructs(t *testing.T) {
|
|
||||||
// Test that the signature structs can be used for ASN.1 operations
|
|
||||||
dsaSig := dsaSignature{
|
|
||||||
R: big.NewInt(12345),
|
|
||||||
S: big.NewInt(67890),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test ASN.1 marshaling
|
|
||||||
data, err := asn1.Marshal(dsaSig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal DSA signature: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test ASN.1 unmarshaling
|
|
||||||
var parsedSig dsaSignature
|
|
||||||
_, err = asn1.Unmarshal(data, &parsedSig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal DSA signature: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify values
|
|
||||||
if dsaSig.R.Cmp(parsedSig.R) != 0 {
|
|
||||||
t.Errorf("R value mismatch: expected %s, got %s", dsaSig.R.String(), parsedSig.R.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if dsaSig.S.Cmp(parsedSig.S) != 0 {
|
|
||||||
t.Errorf("S value mismatch: expected %s, got %s", dsaSig.S.String(), parsedSig.S.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestECDSASignatureStructs(t *testing.T) {
|
|
||||||
// Test that ECDSA signature struct (which is an alias for dsaSignature) works correctly
|
|
||||||
ecdsaSig := ecdsaSignature{
|
|
||||||
R: big.NewInt(99999),
|
|
||||||
S: big.NewInt(11111),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test ASN.1 marshaling
|
|
||||||
data, err := asn1.Marshal(ecdsaSig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal ECDSA signature: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test ASN.1 unmarshaling
|
|
||||||
var parsedSig ecdsaSignature
|
|
||||||
_, err = asn1.Unmarshal(data, &parsedSig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal ECDSA signature: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify values
|
|
||||||
if ecdsaSig.R.Cmp(parsedSig.R) != 0 {
|
|
||||||
t.Errorf("R value mismatch: expected %s, got %s", ecdsaSig.R.String(), parsedSig.R.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if ecdsaSig.S.Cmp(parsedSig.S) != 0 {
|
|
||||||
t.Errorf("S value mismatch: expected %s, got %s", ecdsaSig.S.String(), parsedSig.S.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSigningCertificate_CertificateFields(t *testing.T) {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signerID := "detailed-test@example.com"
|
|
||||||
certDER, err := NewSigningCertificate(signerID, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewSigningCertificate failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test all subject fields
|
|
||||||
expectedSubject := pkix.Name{
|
|
||||||
Organization: []string{"I2P Anonymous Network"},
|
|
||||||
OrganizationalUnit: []string{"I2P"},
|
|
||||||
Locality: []string{"XX"},
|
|
||||||
StreetAddress: []string{"XX"},
|
|
||||||
Country: []string{"XX"},
|
|
||||||
CommonName: signerID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert.Subject.CommonName != expectedSubject.CommonName {
|
|
||||||
t.Errorf("CommonName mismatch: expected %s, got %s", expectedSubject.CommonName, cert.Subject.CommonName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check organization
|
|
||||||
if len(cert.Subject.Organization) != 1 || cert.Subject.Organization[0] != expectedSubject.Organization[0] {
|
|
||||||
t.Errorf("Organization mismatch: expected %v, got %v", expectedSubject.Organization, cert.Subject.Organization)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check organizational unit
|
|
||||||
if len(cert.Subject.OrganizationalUnit) != 1 || cert.Subject.OrganizationalUnit[0] != expectedSubject.OrganizationalUnit[0] {
|
|
||||||
t.Errorf("OrganizationalUnit mismatch: expected %v, got %v", expectedSubject.OrganizationalUnit, cert.Subject.OrganizationalUnit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check locality
|
|
||||||
if len(cert.Subject.Locality) != 1 || cert.Subject.Locality[0] != expectedSubject.Locality[0] {
|
|
||||||
t.Errorf("Locality mismatch: expected %v, got %v", expectedSubject.Locality, cert.Subject.Locality)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check street address
|
|
||||||
if len(cert.Subject.StreetAddress) != 1 || cert.Subject.StreetAddress[0] != expectedSubject.StreetAddress[0] {
|
|
||||||
t.Errorf("StreetAddress mismatch: expected %v, got %v", expectedSubject.StreetAddress, cert.Subject.StreetAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check country
|
|
||||||
if len(cert.Subject.Country) != 1 || cert.Subject.Country[0] != expectedSubject.Country[0] {
|
|
||||||
t.Errorf("Country mismatch: expected %v, got %v", expectedSubject.Country, cert.Subject.Country)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the public key matches
|
|
||||||
certPubKey, ok := cert.PublicKey.(*rsa.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Certificate public key is not RSA")
|
|
||||||
}
|
|
||||||
|
|
||||||
if certPubKey.N.Cmp(privateKey.PublicKey.N) != 0 {
|
|
||||||
t.Error("Certificate public key doesn't match private key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if certPubKey.E != privateKey.PublicKey.E {
|
|
||||||
t.Error("Certificate public key exponent doesn't match private key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark tests for performance validation
|
|
||||||
func BenchmarkNewSigningCertificate(b *testing.B) {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signerID := "benchmark@example.com"
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, err := NewSigningCertificate(signerID, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("NewSigningCertificate failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkCheckSignature(b *testing.B) {
|
|
||||||
// Setup
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := NewSigningCertificate("benchmark@example.com", privateKey)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to create certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testData := []byte("benchmark test data")
|
|
||||||
signature := make([]byte, 256)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = checkSignature(cert, x509.SHA256WithRSA, testData, signature)
|
|
||||||
}
|
|
||||||
}
|
|
151
su3/su3.go
151
su3/su3.go
@@ -12,50 +12,48 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants moved to constants.go
|
const (
|
||||||
|
minVersionLength = 16
|
||||||
|
|
||||||
|
SigTypeDSA = uint16(0)
|
||||||
|
SigTypeECDSAWithSHA256 = uint16(1)
|
||||||
|
SigTypeECDSAWithSHA384 = uint16(2)
|
||||||
|
SigTypeECDSAWithSHA512 = uint16(3)
|
||||||
|
SigTypeRSAWithSHA256 = uint16(4)
|
||||||
|
SigTypeRSAWithSHA384 = uint16(5)
|
||||||
|
SigTypeRSAWithSHA512 = uint16(6)
|
||||||
|
|
||||||
|
ContentTypeUnknown = uint8(0)
|
||||||
|
ContentTypeRouter = uint8(1)
|
||||||
|
ContentTypePlugin = uint8(2)
|
||||||
|
ContentTypeReseed = uint8(3)
|
||||||
|
ContentTypeNews = uint8(4)
|
||||||
|
ContentTypeBlocklist = uint8(5)
|
||||||
|
|
||||||
|
FileTypeZIP = uint8(0)
|
||||||
|
FileTypeXML = uint8(1)
|
||||||
|
FileTypeHTML = uint8(2)
|
||||||
|
FileTypeXMLGZ = uint8(3)
|
||||||
|
FileTypeTXTGZ = uint8(4)
|
||||||
|
FileTypeDMG = uint8(5)
|
||||||
|
FileTypeEXE = uint8(6)
|
||||||
|
|
||||||
|
magicBytes = "I2Psu3"
|
||||||
|
)
|
||||||
|
|
||||||
// File represents a complete SU3 file structure for I2P software distribution.
|
|
||||||
// SU3 files are cryptographically signed containers used to distribute router updates,
|
|
||||||
// plugins, reseed data, and other I2P network components. Each file contains metadata,
|
|
||||||
// content, and a digital signature for verification.
|
|
||||||
type File struct {
|
type File struct {
|
||||||
// Format specifies the SU3 file format version for compatibility tracking
|
Format uint8
|
||||||
Format uint8
|
|
||||||
|
|
||||||
// SignatureType indicates the cryptographic signature algorithm used
|
|
||||||
// Valid values are defined by Sig* constants (RSA, ECDSA, DSA variants)
|
|
||||||
SignatureType uint16
|
SignatureType uint16
|
||||||
|
FileType uint8
|
||||||
|
ContentType uint8
|
||||||
|
|
||||||
// FileType specifies the format of the contained data
|
Version []byte
|
||||||
// Valid values are defined by FileType* constants (ZIP, XML, HTML, etc.)
|
SignerID []byte
|
||||||
FileType uint8
|
Content []byte
|
||||||
|
Signature []byte
|
||||||
// ContentType categorizes the purpose of the contained data
|
|
||||||
// Valid values are defined by ContentType* constants (Router, Plugin, Reseed, etc.)
|
|
||||||
ContentType uint8
|
|
||||||
|
|
||||||
// Version contains version information as bytes, zero-padded to minimum length
|
|
||||||
Version []byte
|
|
||||||
|
|
||||||
// SignerID contains the identity of the entity that signed this file
|
|
||||||
SignerID []byte
|
|
||||||
|
|
||||||
// Content holds the actual file payload data to be distributed
|
|
||||||
Content []byte
|
|
||||||
|
|
||||||
// Signature contains the cryptographic signature for file verification
|
|
||||||
Signature []byte
|
|
||||||
|
|
||||||
// SignedBytes stores the signed portion of the file for verification purposes
|
|
||||||
SignedBytes []byte
|
SignedBytes []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new SU3 file with default settings and current timestamp.
|
|
||||||
// The file is initialized with RSA-SHA512 signature type and a Unix timestamp version.
|
|
||||||
// Additional fields must be set before signing and distribution.
|
|
||||||
// New creates a new SU3 file with default settings and current timestamp.
|
|
||||||
// The file is initialized with RSA-SHA512 signature type and a Unix timestamp version.
|
|
||||||
// Additional fields must be set before signing and distribution.
|
|
||||||
func New() *File {
|
func New() *File {
|
||||||
return &File{
|
return &File{
|
||||||
Version: []byte(strconv.FormatInt(time.Now().Unix(), 10)),
|
Version: []byte(strconv.FormatInt(time.Now().Unix(), 10)),
|
||||||
@@ -63,24 +61,8 @@ func New() *File {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign cryptographically signs the SU3 file using the provided RSA private key.
|
|
||||||
// The signature covers the file header and content but not the signature itself.
|
|
||||||
// The signature length is automatically determined by the RSA key size.
|
|
||||||
// Returns an error if the private key is nil or signature generation fails.
|
|
||||||
func (s *File) Sign(privkey *rsa.PrivateKey) error {
|
func (s *File) Sign(privkey *rsa.PrivateKey) error {
|
||||||
if privkey == nil {
|
|
||||||
lgr.Error("Private key cannot be nil for SU3 signing")
|
|
||||||
return fmt.Errorf("private key cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-calculate signature length to ensure header consistency
|
|
||||||
// This temporary signature ensures BodyBytes() generates correct metadata
|
|
||||||
keySize := privkey.Size() // Returns key size in bytes
|
|
||||||
s.Signature = make([]byte, keySize) // Temporary signature with correct length
|
|
||||||
|
|
||||||
var hashType crypto.Hash
|
var hashType crypto.Hash
|
||||||
// Select appropriate hash algorithm based on signature type
|
|
||||||
// Different signature types require specific hash functions for security
|
|
||||||
switch s.SignatureType {
|
switch s.SignatureType {
|
||||||
case SigTypeDSA:
|
case SigTypeDSA:
|
||||||
hashType = crypto.SHA1
|
hashType = crypto.SHA1
|
||||||
@@ -91,7 +73,6 @@ func (s *File) Sign(privkey *rsa.PrivateKey) error {
|
|||||||
case SigTypeECDSAWithSHA512, SigTypeRSAWithSHA512:
|
case SigTypeECDSAWithSHA512, SigTypeRSAWithSHA512:
|
||||||
hashType = crypto.SHA512
|
hashType = crypto.SHA512
|
||||||
default:
|
default:
|
||||||
lgr.WithField("signature_type", s.SignatureType).Error("Unknown signature type for SU3 signing")
|
|
||||||
return fmt.Errorf("unknown signature type: %d", s.SignatureType)
|
return fmt.Errorf("unknown signature type: %d", s.SignatureType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,11 +80,8 @@ func (s *File) Sign(privkey *rsa.PrivateKey) error {
|
|||||||
h.Write(s.BodyBytes())
|
h.Write(s.BodyBytes())
|
||||||
digest := h.Sum(nil)
|
digest := h.Sum(nil)
|
||||||
|
|
||||||
// Generate RSA signature using PKCS#1 v1.5 padding scheme
|
|
||||||
// The hash type is already applied, so we pass 0 to indicate pre-hashed data
|
|
||||||
sig, err := rsa.SignPKCS1v15(rand.Reader, privkey, 0, digest)
|
sig, err := rsa.SignPKCS1v15(rand.Reader, privkey, 0, digest)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
lgr.WithError(err).Error("Failed to generate RSA signature for SU3 file")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,10 +90,6 @@ func (s *File) Sign(privkey *rsa.PrivateKey) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BodyBytes generates the binary representation of the SU3 file without the signature.
|
|
||||||
// This includes the magic header, metadata fields, and content data in the proper SU3 format.
|
|
||||||
// The signature field length is calculated but the actual signature bytes are not included.
|
|
||||||
// This data is used for signature generation and verification operations.
|
|
||||||
func (s *File) BodyBytes() []byte {
|
func (s *File) BodyBytes() []byte {
|
||||||
var (
|
var (
|
||||||
buf = new(bytes.Buffer)
|
buf = new(bytes.Buffer)
|
||||||
@@ -129,8 +103,7 @@ func (s *File) BodyBytes() []byte {
|
|||||||
contentLength = uint64(len(s.Content))
|
contentLength = uint64(len(s.Content))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Calculate signature length based on algorithm and available signature data
|
// determine sig length based on type
|
||||||
// Different signature types have different length requirements for proper verification
|
|
||||||
switch s.SignatureType {
|
switch s.SignatureType {
|
||||||
case SigTypeDSA:
|
case SigTypeDSA:
|
||||||
signatureLength = uint16(40)
|
signatureLength = uint16(40)
|
||||||
@@ -139,17 +112,10 @@ func (s *File) BodyBytes() []byte {
|
|||||||
case SigTypeECDSAWithSHA384, SigTypeRSAWithSHA384:
|
case SigTypeECDSAWithSHA384, SigTypeRSAWithSHA384:
|
||||||
signatureLength = uint16(384)
|
signatureLength = uint16(384)
|
||||||
case SigTypeECDSAWithSHA512, SigTypeRSAWithSHA512:
|
case SigTypeECDSAWithSHA512, SigTypeRSAWithSHA512:
|
||||||
// For RSA, signature length depends on key size, not hash algorithm
|
signatureLength = uint16(512)
|
||||||
// Use actual signature length if available, otherwise default to 2048-bit RSA
|
|
||||||
if len(s.Signature) > 0 {
|
|
||||||
signatureLength = uint16(len(s.Signature))
|
|
||||||
} else {
|
|
||||||
signatureLength = uint16(256) // Default for 2048-bit RSA key
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure version field meets minimum length requirement by zero-padding
|
// pad the version field
|
||||||
// SU3 specification requires version fields to be at least minVersionLength bytes
|
|
||||||
if len(s.Version) < minVersionLength {
|
if len(s.Version) < minVersionLength {
|
||||||
minBytes := make([]byte, minVersionLength)
|
minBytes := make([]byte, minVersionLength)
|
||||||
copy(minBytes, s.Version)
|
copy(minBytes, s.Version)
|
||||||
@@ -157,8 +123,6 @@ func (s *File) BodyBytes() []byte {
|
|||||||
versionLength = uint8(len(s.Version))
|
versionLength = uint8(len(s.Version))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write SU3 file header in big-endian binary format following specification
|
|
||||||
// Each field is written in the exact order and size required by the SU3 format
|
|
||||||
binary.Write(buf, binary.BigEndian, []byte(magicBytes))
|
binary.Write(buf, binary.BigEndian, []byte(magicBytes))
|
||||||
binary.Write(buf, binary.BigEndian, skip)
|
binary.Write(buf, binary.BigEndian, skip)
|
||||||
binary.Write(buf, binary.BigEndian, s.Format)
|
binary.Write(buf, binary.BigEndian, s.Format)
|
||||||
@@ -181,22 +145,15 @@ func (s *File) BodyBytes() []byte {
|
|||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalBinary serializes the complete SU3 file including signature to binary format.
|
|
||||||
// This produces the final SU3 file data that can be written to disk or transmitted.
|
|
||||||
// The signature must be set before calling this method for a valid SU3 file.
|
|
||||||
func (s *File) MarshalBinary() ([]byte, error) {
|
func (s *File) MarshalBinary() ([]byte, error) {
|
||||||
buf := bytes.NewBuffer(s.BodyBytes())
|
buf := bytes.NewBuffer(s.BodyBytes())
|
||||||
|
|
||||||
// Append signature to complete the SU3 file format
|
// append the signature
|
||||||
// The signature is always the last component of a valid SU3 file
|
|
||||||
binary.Write(buf, binary.BigEndian, s.Signature)
|
binary.Write(buf, binary.BigEndian, s.Signature)
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalBinary deserializes binary data into a SU3 file structure.
|
|
||||||
// This parses the SU3 file format and populates all fields including header metadata,
|
|
||||||
// content, and signature. No validation is performed on the parsed data.
|
|
||||||
func (s *File) UnmarshalBinary(data []byte) error {
|
func (s *File) UnmarshalBinary(data []byte) error {
|
||||||
var (
|
var (
|
||||||
r = bytes.NewReader(data)
|
r = bytes.NewReader(data)
|
||||||
@@ -211,8 +168,6 @@ func (s *File) UnmarshalBinary(data []byte) error {
|
|||||||
contentLength uint64
|
contentLength uint64
|
||||||
)
|
)
|
||||||
|
|
||||||
// Read SU3 file header fields in big-endian format
|
|
||||||
// Each binary.Read operation should be checked for errors in production code
|
|
||||||
binary.Read(r, binary.BigEndian, &magic)
|
binary.Read(r, binary.BigEndian, &magic)
|
||||||
binary.Read(r, binary.BigEndian, &skip)
|
binary.Read(r, binary.BigEndian, &skip)
|
||||||
binary.Read(r, binary.BigEndian, &s.Format)
|
binary.Read(r, binary.BigEndian, &s.Format)
|
||||||
@@ -229,15 +184,11 @@ func (s *File) UnmarshalBinary(data []byte) error {
|
|||||||
binary.Read(r, binary.BigEndian, &s.ContentType)
|
binary.Read(r, binary.BigEndian, &s.ContentType)
|
||||||
binary.Read(r, binary.BigEndian, &bigSkip)
|
binary.Read(r, binary.BigEndian, &bigSkip)
|
||||||
|
|
||||||
// Allocate byte slices based on header length fields
|
|
||||||
// These lengths determine how much data to read for each variable-length field
|
|
||||||
s.Version = make([]byte, versionLength)
|
s.Version = make([]byte, versionLength)
|
||||||
s.SignerID = make([]byte, signerIDLength)
|
s.SignerID = make([]byte, signerIDLength)
|
||||||
s.Content = make([]byte, contentLength)
|
s.Content = make([]byte, contentLength)
|
||||||
s.Signature = make([]byte, signatureLength)
|
s.Signature = make([]byte, signatureLength)
|
||||||
|
|
||||||
// Read variable-length data fields in the order specified by SU3 format
|
|
||||||
// Version, SignerID, Content, and Signature follow the fixed header fields
|
|
||||||
binary.Read(r, binary.BigEndian, &s.Version)
|
binary.Read(r, binary.BigEndian, &s.Version)
|
||||||
binary.Read(r, binary.BigEndian, &s.SignerID)
|
binary.Read(r, binary.BigEndian, &s.SignerID)
|
||||||
binary.Read(r, binary.BigEndian, &s.Content)
|
binary.Read(r, binary.BigEndian, &s.Content)
|
||||||
@@ -246,14 +197,8 @@ func (s *File) UnmarshalBinary(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifySignature validates the SU3 file signature using the provided certificate.
|
|
||||||
// This checks that the signature was created by the private key corresponding to the
|
|
||||||
// certificate's public key. The signature algorithm is determined by the SignatureType field.
|
|
||||||
// Returns an error if verification fails or the signature type is unsupported.
|
|
||||||
func (s *File) VerifySignature(cert *x509.Certificate) error {
|
func (s *File) VerifySignature(cert *x509.Certificate) error {
|
||||||
var sigAlg x509.SignatureAlgorithm
|
var sigAlg x509.SignatureAlgorithm
|
||||||
// Map SU3 signature types to standard x509 signature algorithms
|
|
||||||
// Each SU3 signature type corresponds to a specific combination of algorithm and hash
|
|
||||||
switch s.SignatureType {
|
switch s.SignatureType {
|
||||||
case SigTypeDSA:
|
case SigTypeDSA:
|
||||||
sigAlg = x509.DSAWithSHA1
|
sigAlg = x509.DSAWithSHA1
|
||||||
@@ -270,27 +215,16 @@ func (s *File) VerifySignature(cert *x509.Certificate) error {
|
|||||||
case SigTypeRSAWithSHA512:
|
case SigTypeRSAWithSHA512:
|
||||||
sigAlg = x509.SHA512WithRSA
|
sigAlg = x509.SHA512WithRSA
|
||||||
default:
|
default:
|
||||||
lgr.WithField("signature_type", s.SignatureType).Error("Unknown signature type for SU3 verification")
|
|
||||||
return fmt.Errorf("unknown signature type: %d", s.SignatureType)
|
return fmt.Errorf("unknown signature type: %d", s.SignatureType)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := checkSignature(cert, sigAlg, s.BodyBytes(), s.Signature)
|
return checkSignature(cert, sigAlg, s.BodyBytes(), s.Signature)
|
||||||
if err != nil {
|
|
||||||
lgr.WithError(err).WithField("signature_type", s.SignatureType).Error("SU3 signature verification failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a human-readable representation of the SU3 file metadata.
|
|
||||||
// This includes format information, signature type, file type, content type, version,
|
|
||||||
// and signer ID in a formatted display suitable for debugging and verification.
|
|
||||||
func (s *File) String() string {
|
func (s *File) String() string {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
||||||
// Format SU3 file metadata in a readable table structure
|
// header
|
||||||
// Display key fields with proper formatting and null-byte trimming
|
|
||||||
fmt.Fprintln(&b, "---------------------------")
|
fmt.Fprintln(&b, "---------------------------")
|
||||||
fmt.Fprintf(&b, "Format: %q\n", s.Format)
|
fmt.Fprintf(&b, "Format: %q\n", s.Format)
|
||||||
fmt.Fprintf(&b, "SignatureType: %q\n", s.SignatureType)
|
fmt.Fprintf(&b, "SignatureType: %q\n", s.SignatureType)
|
||||||
@@ -300,8 +234,7 @@ func (s *File) String() string {
|
|||||||
fmt.Fprintf(&b, "SignerId: %q\n", s.SignerID)
|
fmt.Fprintf(&b, "SignerId: %q\n", s.SignerID)
|
||||||
fmt.Fprintf(&b, "---------------------------")
|
fmt.Fprintf(&b, "---------------------------")
|
||||||
|
|
||||||
// Content and signature data are commented out to avoid large output
|
// content & signature
|
||||||
// Uncomment these lines for debugging when full content inspection is needed
|
|
||||||
// fmt.Fprintf(&b, "Content: %q\n", s.Content)
|
// fmt.Fprintf(&b, "Content: %q\n", s.Content)
|
||||||
// fmt.Fprintf(&b, "Signature: %q\n", s.Signature)
|
// fmt.Fprintf(&b, "Signature: %q\n", s.Signature)
|
||||||
// fmt.Fprintln(&b, "---------------------------")
|
// fmt.Fprintln(&b, "---------------------------")
|
||||||
|
541
su3/su3_test.go
541
su3/su3_test.go
@@ -1,541 +0,0 @@
|
|||||||
package su3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
file := New()
|
|
||||||
|
|
||||||
if file == nil {
|
|
||||||
t.Fatal("New() returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.SignatureType != SigTypeRSAWithSHA512 {
|
|
||||||
t.Errorf("Expected SignatureType %d, got %d", SigTypeRSAWithSHA512, file.SignatureType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(file.Version) == 0 {
|
|
||||||
t.Error("Version should be set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify version is a valid Unix timestamp string
|
|
||||||
if len(file.Version) < 10 {
|
|
||||||
t.Error("Version should be at least 10 characters (Unix timestamp)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_Sign(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
signatureType uint16
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "RSA with SHA256",
|
|
||||||
signatureType: SigTypeRSAWithSHA256,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "RSA with SHA384",
|
|
||||||
signatureType: SigTypeRSAWithSHA384,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "RSA with SHA512",
|
|
||||||
signatureType: SigTypeRSAWithSHA512,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Unknown signature type",
|
|
||||||
signatureType: uint16(999),
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate test RSA key
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
file := New()
|
|
||||||
file.SignatureType = tt.signatureType
|
|
||||||
file.Content = []byte("test content")
|
|
||||||
file.SignerID = []byte("test@example.com")
|
|
||||||
|
|
||||||
err := file.Sign(privateKey)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error but got none")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(file.Signature) == 0 {
|
|
||||||
t.Error("Signature should be set after signing")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_Sign_NilPrivateKey(t *testing.T) {
|
|
||||||
file := New()
|
|
||||||
file.Content = []byte("test content")
|
|
||||||
|
|
||||||
err := file.Sign(nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error when signing with nil private key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_BodyBytes(t *testing.T) {
|
|
||||||
file := New()
|
|
||||||
file.Format = 1
|
|
||||||
file.SignatureType = SigTypeRSAWithSHA256
|
|
||||||
file.FileType = FileTypeZIP
|
|
||||||
file.ContentType = ContentTypeReseed
|
|
||||||
file.Version = []byte("1234567890")
|
|
||||||
file.SignerID = []byte("test@example.com")
|
|
||||||
file.Content = []byte("test content data")
|
|
||||||
|
|
||||||
bodyBytes := file.BodyBytes()
|
|
||||||
|
|
||||||
if len(bodyBytes) == 0 {
|
|
||||||
t.Error("BodyBytes should not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that magic bytes are included
|
|
||||||
if !bytes.HasPrefix(bodyBytes, []byte(magicBytes)) {
|
|
||||||
t.Error("BodyBytes should start with magic bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test version padding
|
|
||||||
shortVersionFile := New()
|
|
||||||
shortVersionFile.Version = []byte("123") // Less than minVersionLength
|
|
||||||
bodyBytes = shortVersionFile.BodyBytes()
|
|
||||||
|
|
||||||
if len(bodyBytes) == 0 {
|
|
||||||
t.Error("BodyBytes should handle short version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_MarshalBinary(t *testing.T) {
|
|
||||||
file := New()
|
|
||||||
file.Content = []byte("test content")
|
|
||||||
file.SignerID = []byte("test@example.com")
|
|
||||||
file.Signature = []byte("dummy signature data")
|
|
||||||
|
|
||||||
data, err := file.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("MarshalBinary failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) == 0 {
|
|
||||||
t.Error("MarshalBinary should return data")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature is at the end
|
|
||||||
expectedSigStart := len(data) - len(file.Signature)
|
|
||||||
if !bytes.Equal(data[expectedSigStart:], file.Signature) {
|
|
||||||
t.Error("Signature should be at the end of marshaled data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_UnmarshalBinary(t *testing.T) {
|
|
||||||
// Create a file and marshal it
|
|
||||||
originalFile := New()
|
|
||||||
originalFile.Format = 1
|
|
||||||
originalFile.SignatureType = SigTypeRSAWithSHA256
|
|
||||||
originalFile.FileType = FileTypeZIP
|
|
||||||
originalFile.ContentType = ContentTypeReseed
|
|
||||||
originalFile.Version = []byte("1234567890123456") // Exactly minVersionLength
|
|
||||||
originalFile.SignerID = []byte("test@example.com")
|
|
||||||
originalFile.Content = []byte("test content data")
|
|
||||||
originalFile.Signature = make([]byte, 256) // Appropriate size for RSA SHA256
|
|
||||||
|
|
||||||
// Fill signature with test data
|
|
||||||
for i := range originalFile.Signature {
|
|
||||||
originalFile.Signature[i] = byte(i % 256)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := originalFile.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal test file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal into new file
|
|
||||||
newFile := &File{}
|
|
||||||
err = newFile.UnmarshalBinary(data)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("UnmarshalBinary failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare fields
|
|
||||||
if newFile.Format != originalFile.Format {
|
|
||||||
t.Errorf("Format mismatch: expected %d, got %d", originalFile.Format, newFile.Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newFile.SignatureType != originalFile.SignatureType {
|
|
||||||
t.Errorf("SignatureType mismatch: expected %d, got %d", originalFile.SignatureType, newFile.SignatureType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newFile.FileType != originalFile.FileType {
|
|
||||||
t.Errorf("FileType mismatch: expected %d, got %d", originalFile.FileType, newFile.FileType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newFile.ContentType != originalFile.ContentType {
|
|
||||||
t.Errorf("ContentType mismatch: expected %d, got %d", originalFile.ContentType, newFile.ContentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(newFile.Version, originalFile.Version) {
|
|
||||||
t.Errorf("Version mismatch: expected %s, got %s", originalFile.Version, newFile.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(newFile.SignerID, originalFile.SignerID) {
|
|
||||||
t.Errorf("SignerID mismatch: expected %s, got %s", originalFile.SignerID, newFile.SignerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(newFile.Content, originalFile.Content) {
|
|
||||||
t.Errorf("Content mismatch: expected %s, got %s", originalFile.Content, newFile.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(newFile.Signature, originalFile.Signature) {
|
|
||||||
t.Error("Signature mismatch")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_UnmarshalBinary_InvalidData(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Empty data",
|
|
||||||
data: []byte{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Too short data",
|
|
||||||
data: []byte("short"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid magic bytes",
|
|
||||||
data: append([]byte("BADMAG"), make([]byte, 100)...),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
file := &File{}
|
|
||||||
err := file.UnmarshalBinary(tt.data)
|
|
||||||
// Note: The current implementation doesn't validate magic bytes or handle errors gracefully
|
|
||||||
// This test documents the current behavior
|
|
||||||
_ = err // We expect this might fail, but we're testing it doesn't panic
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_VerifySignature(t *testing.T) {
|
|
||||||
// Generate test certificate and private key
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a test certificate
|
|
||||||
cert, err := NewSigningCertificate("test@example.com", privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedCert, err := x509.ParseCertificate(cert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse test certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
signatureType uint16
|
|
||||||
setupFile func(*File)
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid RSA SHA256 signature",
|
|
||||||
signatureType: SigTypeRSAWithSHA256,
|
|
||||||
setupFile: func(f *File) {
|
|
||||||
f.Content = []byte("test content")
|
|
||||||
f.SignerID = []byte("test@example.com")
|
|
||||||
err := f.Sign(privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to sign file: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Unknown signature type",
|
|
||||||
signatureType: uint16(999),
|
|
||||||
setupFile: func(f *File) {
|
|
||||||
f.Content = []byte("test content")
|
|
||||||
f.SignerID = []byte("test@example.com")
|
|
||||||
f.Signature = []byte("dummy signature")
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
file := New()
|
|
||||||
file.SignatureType = tt.signatureType
|
|
||||||
tt.setupFile(file)
|
|
||||||
|
|
||||||
err := file.VerifySignature(parsedCert)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error but got none")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_String(t *testing.T) {
|
|
||||||
file := New()
|
|
||||||
file.Format = 1
|
|
||||||
file.SignatureType = SigTypeRSAWithSHA256
|
|
||||||
file.FileType = FileTypeZIP
|
|
||||||
file.ContentType = ContentTypeReseed
|
|
||||||
file.Version = []byte("test version")
|
|
||||||
file.SignerID = []byte("test@example.com")
|
|
||||||
|
|
||||||
str := file.String()
|
|
||||||
|
|
||||||
if len(str) == 0 {
|
|
||||||
t.Error("String() should not return empty string")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that important fields are included in string representation
|
|
||||||
expectedSubstrings := []string{
|
|
||||||
"Format:",
|
|
||||||
"SignatureType:",
|
|
||||||
"FileType:",
|
|
||||||
"ContentType:",
|
|
||||||
"Version:",
|
|
||||||
"SignerId:",
|
|
||||||
"---------------------------",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, substr := range expectedSubstrings {
|
|
||||||
if !strings.Contains(str, substr) {
|
|
||||||
t.Errorf("String() should contain '%s'", substr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstants(t *testing.T) {
|
|
||||||
// Test that constants have expected values
|
|
||||||
if magicBytes != "I2Psu3" {
|
|
||||||
t.Errorf("Expected magic bytes 'I2Psu3', got '%s'", magicBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if minVersionLength != 16 {
|
|
||||||
t.Errorf("Expected minVersionLength 16, got %d", minVersionLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test signature type constants
|
|
||||||
expectedSigTypes := map[string]uint16{
|
|
||||||
"DSA": 0,
|
|
||||||
"ECDSAWithSHA256": 1,
|
|
||||||
"ECDSAWithSHA384": 2,
|
|
||||||
"ECDSAWithSHA512": 3,
|
|
||||||
"RSAWithSHA256": 4,
|
|
||||||
"RSAWithSHA384": 5,
|
|
||||||
"RSAWithSHA512": 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
actualSigTypes := map[string]uint16{
|
|
||||||
"DSA": SigTypeDSA,
|
|
||||||
"ECDSAWithSHA256": SigTypeECDSAWithSHA256,
|
|
||||||
"ECDSAWithSHA384": SigTypeECDSAWithSHA384,
|
|
||||||
"ECDSAWithSHA512": SigTypeECDSAWithSHA512,
|
|
||||||
"RSAWithSHA256": SigTypeRSAWithSHA256,
|
|
||||||
"RSAWithSHA384": SigTypeRSAWithSHA384,
|
|
||||||
"RSAWithSHA512": SigTypeRSAWithSHA512,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(expectedSigTypes, actualSigTypes) {
|
|
||||||
t.Error("Signature type constants don't match expected values")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_RoundTrip(t *testing.T) {
|
|
||||||
// Test complete round-trip: create -> sign -> marshal -> unmarshal -> verify
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := NewSigningCertificate("roundtrip@example.com", privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedCert, err := x509.ParseCertificate(cert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and set up original file
|
|
||||||
originalFile := New()
|
|
||||||
originalFile.FileType = FileTypeZIP
|
|
||||||
originalFile.ContentType = ContentTypeReseed
|
|
||||||
originalFile.Content = []byte("This is test content for round-trip testing")
|
|
||||||
originalFile.SignerID = []byte("roundtrip@example.com")
|
|
||||||
|
|
||||||
// Sign the file
|
|
||||||
err = originalFile.Sign(privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to sign file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal to binary
|
|
||||||
data, err := originalFile.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal from binary
|
|
||||||
newFile := &File{}
|
|
||||||
err = newFile.UnmarshalBinary(data)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature
|
|
||||||
err = newFile.VerifySignature(parsedCert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to verify signature: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure content matches
|
|
||||||
if !bytes.Equal(originalFile.Content, newFile.Content) {
|
|
||||||
t.Error("Content doesn't match after round-trip")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(originalFile.SignerID, newFile.SignerID) {
|
|
||||||
t.Error("SignerID doesn't match after round-trip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFile_Sign_RSAKeySize(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
keySize int
|
|
||||||
expectedSigLen int
|
|
||||||
}{
|
|
||||||
{"2048-bit RSA", 2048, 256},
|
|
||||||
{"3072-bit RSA", 3072, 384},
|
|
||||||
{"4096-bit RSA", 4096, 512},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// Generate RSA key of specific size
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, tc.keySize)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to generate %d-bit RSA key: %v", tc.keySize, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
file := New()
|
|
||||||
file.Content = []byte("test content")
|
|
||||||
file.SignerID = []byte("test@example.com")
|
|
||||||
file.SignatureType = SigTypeRSAWithSHA512
|
|
||||||
|
|
||||||
err = file.Sign(privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error signing with %d-bit key: %v", tc.keySize, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(file.Signature) != tc.expectedSigLen {
|
|
||||||
t.Errorf("Expected signature length %d for %d-bit key, got %d",
|
|
||||||
tc.expectedSigLen, tc.keySize, len(file.Signature))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the header reflects the correct signature length
|
|
||||||
bodyBytes := file.BodyBytes()
|
|
||||||
if len(bodyBytes) == 0 {
|
|
||||||
t.Error("BodyBytes should not be empty")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark tests for performance validation
|
|
||||||
func BenchmarkNew(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = New()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkFile_BodyBytes(b *testing.B) {
|
|
||||||
file := New()
|
|
||||||
file.Content = make([]byte, 1024) // 1KB content
|
|
||||||
file.SignerID = []byte("benchmark@example.com")
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = file.BodyBytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkFile_MarshalBinary(b *testing.B) {
|
|
||||||
file := New()
|
|
||||||
file.Content = make([]byte, 1024) // 1KB content
|
|
||||||
file.SignerID = []byte("benchmark@example.com")
|
|
||||||
file.Signature = make([]byte, 512)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = file.MarshalBinary()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkFile_UnmarshalBinary(b *testing.B) {
|
|
||||||
// Create test data once
|
|
||||||
file := New()
|
|
||||||
file.Content = make([]byte, 1024)
|
|
||||||
file.SignerID = []byte("benchmark@example.com")
|
|
||||||
file.Signature = make([]byte, 512)
|
|
||||||
|
|
||||||
data, err := file.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to create test data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
newFile := &File{}
|
|
||||||
_ = newFile.UnmarshalBinary(data)
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user