Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
septs committed Oct 6, 2022
0 parents commit b96dbe7
Show file tree
Hide file tree
Showing 29 changed files with 896 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2021 NiceLabs

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.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Local hook driven server

## Worker Service

1. [FTP worker](./cmd/ftp-worker)
1. [TFTP worker](./cmd/tftp-worker)
1. [SMTP worker](./cmd/smtp-worker)

## LICENSE

[LICENSE](LICENSE)
22 changes: 22 additions & 0 deletions cmd/ftp-worker/EXAMPLE-JUNOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# JunOS Configuration Archival

Based [Backup Configurations to an Archive Site][backup-configuration]

[backup-configuration]: https://www.juniper.net/documentation/en_US/junos/topics/task/configuration/junos-software-system-management-router-configuration-archiving.html

## Local side

```bash
ftp-worker \
-on-write ./on-write-junos-archive.py \
-workdir path/to/git-repo
```

## JunOS side

Based `transfer-on-commit` command automate backup JunOS configuration to remote-side

```plain
set system archival configuration transfer-on-commit
set system archival configuration archive-sites ftp://<username>@<host>:<port>/<path> password <password>
```
42 changes: 42 additions & 0 deletions cmd/ftp-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# FTP Worker

The [FTP](https://www.rfc-editor.org/rfc/rfc959) worker

Redirect upload (write operation) and download (read operation) as local program calling

## Usage

### Read hook

Equivalent to:

```plain
read-hook-program <filename> | <file-content>
```

Environions:

| Field Name | Type |
| ----------------- | --------- |
| `FTP_ACTION` | `READ` |
| `FTP_PATH` | file path |
| `FTP_READ_OFFSET` | integer |

### Write hook

Equivalent to:

```plain
<file-content> | write-hook-program <filename>
```

Environments:

| Field Name | Type |
| ------------ | --------- |
| `FTP_ACTION` | `WRITE` |
| `FTP_PATH` | file path |

## Example

- [JunOS Configuration Archival](EXAMPLE-JUNOS.md)
98 changes: 98 additions & 0 deletions cmd/ftp-worker/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package main

import (
"bytes"
"errors"
"io"
"log"
"os"
"os/exec"
"strconv"

"github.com/NiceLabs/hook-driven-server/utils"
"goftp.io/server/core"
)

type HookDriver struct {
Workdir string
ReadHook string
WriteHook string
writeMaps map[string]io.ReaderFrom
}

func (h *HookDriver) Stat(path string) (core.FileInfo, error) {
return DummyFileInfo(path), nil
}

func (h *HookDriver) ListDir(string, func(core.FileInfo) error) error {
return errors.New("ftp-worker: list directory operation not permitted")
}

func (h *HookDriver) DeleteDir(string) error {
return errors.New("ftp-worker: delete directory operation not permitted")
}

func (h *HookDriver) DeleteFile(string) error {
return errors.New("ftp-worker: delete file operation not permitted")
}

func (h *HookDriver) Rename(string, string) error {
return errors.New("ftp-worker: rename operation not permitted")
}

func (h *HookDriver) MakeDir(string) error {
return errors.New("ftp-worker: make directory operation not permitted")
}

func (h *HookDriver) GetFile(path string, offset int64) (n int64, stdout io.ReadCloser, err error) {
log.Printf("Read %q with %d offset\n", path, offset)
if h.ReadHook == "" {
err = errors.New("ftp-worker: read operation not permitted")
return
}
cmd := exec.Command(h.ReadHook, path, strconv.FormatInt(offset, 64))
utils.AddEnv(cmd, map[string]string{
"FTP_ACTION": "READ",
"FTP_PATH": path,
"FTP_READ_OFFSET": strconv.FormatInt(offset, 64),
})
cmd.Dir = h.Workdir
cmd.Stderr = os.Stderr
if err = cmd.Start(); err != nil {
return
}
stdout, err = cmd.StdoutPipe()
return
}

func (h *HookDriver) PutFile(destPath string, data io.Reader, appendData bool) (n int64, err error) {
if appendData {
log.Printf("Write %q\n with append data", destPath)
} else {
log.Printf("Write %q\n", destPath)
}
if h.WriteHook == "" {
err = errors.New("ftp-worker: write operation not permitted")
return
}
if appendData && h.writeMaps[destPath] != nil {
return h.writeMaps[destPath].ReadFrom(data)
}
cmd := exec.Command(h.WriteHook, destPath)
utils.AddEnv(cmd, map[string]string{
"FTP_ACTION": "WRITE",
"FTP_PATH": destPath,
})
cmd.Dir = h.Workdir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
var stdin bytes.Buffer
n, err = stdin.ReadFrom(data)
cmd.Stdin = &stdin
h.writeMaps[destPath] = &stdin
go func() {
_ = cmd.Run()
delete(h.writeMaps, destPath)
}()
return
}
19 changes: 19 additions & 0 deletions cmd/ftp-worker/dummy_file_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import (
"io/fs"
"os"
"strings"
"time"
)

type DummyFileInfo string

func (d DummyFileInfo) Name() string { return string(d) }
func (d DummyFileInfo) Size() int64 { return 0 }
func (d DummyFileInfo) Mode() fs.FileMode { return os.ModeTemporary }
func (d DummyFileInfo) ModTime() time.Time { return time.UnixMilli(0) }
func (d DummyFileInfo) IsDir() bool { return strings.HasSuffix(string(d), "/") }
func (d DummyFileInfo) Sys() any { return nil }
func (d DummyFileInfo) Owner() string { return "nobody" }
func (d DummyFileInfo) Group() string { return "nobody" }
23 changes: 23 additions & 0 deletions cmd/ftp-worker/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"io"

"goftp.io/server/core"
)

type HookDriverFactory struct {
Workdir string
ReadHook string
WriteHook string
}

func (h *HookDriverFactory) NewDriver() (driver core.Driver, err error) {
driver = &HookDriver{
Workdir: h.Workdir,
ReadHook: h.ReadHook,
WriteHook: h.WriteHook,
writeMaps: make(map[string]io.ReaderFrom),
}
return
}
49 changes: 49 additions & 0 deletions cmd/ftp-worker/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"flag"
"log"

. "goftp.io/server"
)

var (
hostname string
port int
factory = new(HookDriverFactory)
auth = new(SimpleAuth)
certFile string
keyFile string
explicitFTPS bool
)

func init() {
flag.StringVar(&hostname, "hostname", "", "FTP listen hostname")
flag.IntVar(&port, "port", 21, "FTP listen port")
flag.StringVar(&certFile, "tls-cert-file", "", "TLS Certificate file")
flag.StringVar(&keyFile, "tls-key-file", "", "TLS Key file")
flag.BoolVar(&explicitFTPS, "tls-explicit", false, "Explicit FTPS")
flag.StringVar(&auth.Name, "username", "user", "Username")
flag.StringVar(&auth.Password, "password", "pass", "Password")
flag.StringVar(&factory.Workdir, "workdir", "", "Work directory")
flag.StringVar(&factory.ReadHook, "on-read", "", "Read operation local hook program")
flag.StringVar(&factory.WriteHook, "on-write", "", "Write operation local hook program")
flag.Parse()
}

func main() {
serve := NewServer(&ServerOpts{
Name: "FTP Hook Service",
Auth: auth,
Hostname: hostname,
Port: port,
TLS: certFile != "" && keyFile != "",
CertFile: certFile,
KeyFile: keyFile,
ExplicitFTPS: explicitFTPS,
Factory: factory,
})
if err := serve.ListenAndServe(); err != nil {
log.Fatal("Error starting server:", err)
}
}
36 changes: 36 additions & 0 deletions cmd/ftp-worker/on-write-junos-archive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import gzip
import os
import re
import sys
from subprocess import call

# pattern: <router-name>_YYYYMMDD_HHMMSS_juniper.conf<.n>.gz
re_filename = re.compile(r'^(\S+)?_\d{8}_\d{6}_juniper\.conf(?:\.\d+)?\.gz$')


def main(filename: str):
matched = re_filename.match(os.path.basename(filename))
if not matched:
sys.exit(1)
saved_filename = '%s.conf' % (matched.group(1) or 'unnamed')
decompressed = gzip.decompress(sys.stdin.buffer.read()).decode()
if not has_changed(saved_filename, decompressed):
return
with open(os.path.join(saved_filename), 'w') as fp:
fp.write(decompressed)
call(['git', 'add', saved_filename])
call(['git', 'commit', '-m', filename])
call(['git', 'push'])


def has_changed(filename: str, decompressed: str):
if not os.path.exists(filename):
return True
with open(filename, 'r') as fp:
original = fp.read()
return original.splitlines()[1:] != decompressed.splitlines()[1:]


if __name__ == '__main__':
main(sys.argv[1])
67 changes: 67 additions & 0 deletions cmd/smtp-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# SMTP worker

The SMTP worker

Redirect SMTP request to local hook program.

## Usage

<!-- markdownlint-disable -->

```plain
Usage of smtp-worker:
-addr string
SMTP listen address
-on-request string
SMTP request handler
-tls-cert-file string
TLS Certificate file
-tls-key-file string
TLS Key file
-username string
Username (default "user")
-password string
Password (default "pass")
```

<!-- markdownlint-restore -->

### On Request

Equivalent to:

```plain
<body> | hook-program <to-address> <from-address>
```

Environments:

| Field Name | Type |
| --------------------- | ------------------- |
| `SMTP_FROM` | string |
| `SMTP_FROM_USERNAME` | string |
| `SMTP_FROM_DOMAIN` | string (fqdn) |
| `SMTP_TO` | string |
| `SMTP_TO_USERNAME` | string |
| `SMTP_TO_DOMAIN` | string (fqdn) |
| `SMTP_TO_DOMAIN_TYPE` | string |
| `SMTP_HOSTNAME` | string |
| `SMTP_LOCAL_ADDR` | string (ip address) |
| `SMTP_REMOTE_ADDR` | string (ip address) |
| `SMTP_UTF8` | boolean |
| `SMTP_REQUIRE_TLS` | boolean |
| `SMTP_BODY_TYPE` | string |
| `SMTP_BODY_SIZE` | interge |

`SMTP_TO_DOMAIN_TYPE`:

- `DISPOSABLE_MAIL`, disposable email provides
- `FREE_MAIL`, free email provides
- `SWOT_MAIL`, education email domains
- `DDNS`, dynamic dns provides

`SMTP_BODY_TYPE`:

- `7BIT`, 7bit, e.g: UTF-7, Base64 or Quoted-Printable Encoding
- `8BITMIME`, 8bit MIME
- `BINARYMIME`, Binary MIME
Loading

0 comments on commit b96dbe7

Please sign in to comment.