...
 
Commits (2)
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
*.env
*_manifest.json
*_chart.json
.idea/
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
MIT License
Copyright (c) 2020 Sunrise Project
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.
# Sunrise Manifest Builder
CLI tool to build a json manifest
## Summary
The builder tool creates a manifest json file that by walking the files specified by the chart file.
## Command Line Arguments
```
builder <chart file>
```
## Chart File
The chart file supplies configuration to the builder on how it should create the manifest file.
metadata (object) - Metadata for the manifest. If 'version' is omitted a hash will be automatically generated based on the contents of the manifest instead.
base_path (string) - This is path where the builder will walk files to build the manifest.
ignore (string array) - Files or directories containing any of these strings will be ignored while building the manifest.
contains (string array) - If specified, only files or directories containing any of these strings will added to the manifest.
replace (object array) - Modifies the file path by replacing where there is a 'match' with the 'value'.
sources (object array) - Url(s) to join the files found to generate the files sources. In addition, you can add ignore, contains, and replace modifiers to individual sources.
files (object array) - These are static file entries included in the manifest.
## Sample Chart File (Rebirth)
```
{
"metadata": {
"name": "rebirth",
"title": "City of Heroes: Rebirth",
"desc": "City of Heroes: Rebirth",
"version": "1.1.1",
"welcome_url": "https://forum.cityofheroesrebirth.com",
"launch_options" : [
{
"name" : "Rebirth",
"path" : "Ouroboros.exe",
"args" : "-patchDir rebirth -auth i24.cityofheroesrebirth.com"
}
]
},
"base_path" : "D:/rebirth",
"ignore": [
"Costumes/",
"logs/",
"rebirthtest"
],
"sources" : [
{
"url": "https://brute.coh.network/OuroDev/v2i1",
"replace" : [
{
"match": "OuroDev/v2i1/rebirth/",
"value": "rebirth/"
}
]
}
],
"files" : [
{
"path": "rebirth/ouro_bin_v2i1.pigg",
"size": 0,
"sources": []
},
{
"path": "rebirth/rebirth-bin.pigg",
"size": 0,
"sources": []
},
{
"path": "rebirth/rebirth-ogg.pigg",
"size": 0,
"sources": []
},
{
"path": "rebirth/rebirth-tex.pigg",
"size": 0,
"sources": []
},
{
"path": "rebirth.exe",
"size": 0,
"sources": []
}
]
}rebi
```
## Sample Chart File (Homecoming)
```
{
"metadata": {
"name": "homecoming64",
"desc": "Homecoming Issue #26",
"welcome_url": "https://patch.savecoh.com/homecoming.html",
"launch_options" : [
{
"name" : "Homecoming (64-bit)",
"path" : "hc-bin64\cityofheroes.exe",
"args" : "-patchversion 20200417_1390 -auth 51.161.76.201 -patchdir homecoming"
},
{
"name" : "Homecoming (32-bit)",
"path" : "hc-bin32\cityofheroes.exe",
"args" : "-patchversion 20200417_1390 -auth 51.161.76.201 -patchdir homecoming"
}
]
},
"base_path" : "C:/CoH/HC",
"contains" : [
"hc-bin64/",
"hc-bin32/",
"homecoming/",
"piggs/"
],
"replace": [
{
"match": "homecoming/",
"value": "patch/"
}
],
"sources" : [
{
"url": "http://cdn.homecomingservers.com/hc/20200303_848"
},
{
"url": "http://nsa.tools/hc/20200303_848"
}
]
}
```
\ No newline at end of file
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"git.ourodev.com/community/sunrise-api/manifest"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage:")
fmt.Println("builder <manifest_chart>")
return
}
loadBuildChart(os.Args[1])
m := manifest.Manifest{}
m.Metadata = buildChart.Metadata
err := filepath.Walk(buildChart.BasePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
relpath, err := filepath.Rel(buildChart.BasePath, path)
if err != nil {
return err
}
relpath = strings.ReplaceAll(relpath, "\\", "/")
file := manifest.File{
Path: relpath,
Size: info.Size(),
}
ok, relpath := buildChart.CIR.apply(relpath)
if !ok {
return nil
}
file.SHA256, err = getSha256(path)
if err != nil {
return err
}
//add each source
for _, source := range buildChart.Sources {
url := fmt.Sprintf("%v/%v", source.URL, relpath)
_, url = source.CIR.apply(url)
file.Sources = append(file.Sources, manifest.Source{URL: url})
}
m.Files = append(m.Files, file)
fmt.Println(path, info.Size())
return nil
})
for _, file := range buildChart.Files {
m.Files = append(m.Files, file)
}
if err != nil {
fmt.Println(err)
return
}
if err := buildVersionHash(&m); err != nil {
fmt.Println(err)
return
}
b, err := json.MarshalIndent(&m, "", "\t")
if err != nil {
fmt.Println(err)
}
ioutil.WriteFile(m.Metadata.Name+"_manifest.json", b, 0644)
}
func getSha256(filepath string) (string, error) {
f, err := os.Open(filepath)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
var buildChart struct {
BasePath string `json:"base_path"`
Metadata manifest.Metadata `json:"metadata"`
Output string `json:"output"`
CIR
Sources []struct {
URL string `json:"url"`
CIR
} `json:"Sources"`
Files []manifest.File `json:"files"`
}
type CIR struct {
Contains []string `json:"contains"`
Ignore []string `json:"ignore"`
Replace []struct {
Match string `json:"match"`
Value string `json:"value"`
} `json:"replace"`
}
func (cir CIR) apply(s string) (bool, string) {
if len(cir.Contains) != 0 {
if ok := contains(s, cir.Contains); !ok {
return false, ""
}
}
if ok := contains(s, cir.Ignore); ok {
return false, ""
}
for _, replace := range cir.Replace {
s = strings.ReplaceAll(s, replace.Match, replace.Value)
}
return true, s
}
func loadBuildChart(path string) error {
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if err := json.Unmarshal(b, &buildChart); err != nil {
return err
}
return nil
}
func contains(s string, match []string) bool {
for i := range match {
if strings.Contains(s, match[i]) {
return true
}
}
return false
}
func buildVersionHash(m *manifest.Manifest) error {
if m.Version != "" {
return nil
}
b, err := json.Marshal(m)
if err != nil {
return err
}
buf := bytes.NewBuffer(b)
h := sha256.New()
if _, err := io.Copy(h, buf); err != nil {
return err
}
m.Version = hex.EncodeToString(h.Sum(nil))
return nil
}
# Sunrise API MongoDB
Sunrise manifest API, backed by MongoDB
## Environment Variables
Sunrise API will read the .env file in your application folder on startup and assign environment variables.
`PORT` Port to listen on for http requests, must begin with ":".
`CERT_FILE` Path to cert file, required for TLS.
`KEY_FILE` Path to key file, required for TLS.
`MONGODB_URI` URI to connect to Mongo DB, See: <https://docs.mongodb.com/manual/reference/connection-string/>
`MONGODB_DB` Mongo database name of the collection below.
`MONGODB_COLLECTION` Mongo collection containing the manifest documents.
## Resource Paths
```
GET /manifest/{name}
Returns the entire manifest.
GET /manifests/metadata
Returns the metadata of all available manifests.
GET /manifest/{name}/metadata
Returns the metadata of the manifest.
```
\ No newline at end of file
package main
import (
"context"
"log"
"os"
"git.ourodev.com/community/sunrise-api/srv"
"github.com/joho/godotenv"
)
func main() {
if err := loadConfig(); err != nil {
log.Fatal(err)
}
repo, err := newMongoRepo()
if err != nil {
log.Fatal(err)
}
defer repo.client.Disconnect(context.Background())
config.srvConfig.Repo = repo
if err := srv.ListenAndServe(config.srvConfig); err != nil {
log.Fatal(err)
}
}
var config struct {
srvConfig srv.Config
mongoDbURI string
mongoDbDatabase string
mongoDbCollection string
}
func loadConfig() error {
if err := godotenv.Load(); err != nil {
return err
}
config.srvConfig.Load()
config.mongoDbURI = os.Getenv("MONGODB_URI")
config.mongoDbDatabase = os.Getenv("MONGODB_DB")
config.mongoDbCollection = os.Getenv("MONGODB_COLLECTION")
return nil
}
package main
import (
"context"
"git.ourodev.com/community/sunrise-api/manifest"
"go.mongodb.org/mongo-driver/bson"
bsonp "go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type mongoRepo struct {
client *mongo.Client
collection *mongo.Collection
}
func newMongoRepo() (*mongoRepo, error) {
clientOptions := options.Client().ApplyURI(config.mongoDbURI)
client, err := mongo.NewClient(clientOptions)
if err != nil {
return nil, err
}
err = client.Connect(context.Background())
if err != nil {
return nil, err
}
db := client.Database(config.mongoDbDatabase)
col := db.Collection(config.mongoDbCollection)
return &mongoRepo{
client: client,
collection: col,
}, nil
}
func (m mongoRepo) FindAllManifestMetaData() ([]manifest.Metadata, error) {
opts := options.Find()
opts.Projection = bson.D{
bsonp.E{Key: "files", Value: 0},
}
cursor, err := m.collection.Find(context.Background(), bson.D{}, opts)
if err != nil {
return nil, err
}
manifests := make([]manifest.Metadata, 0)
if err := cursor.All(context.Background(), &manifests); err != nil {
return nil, err
}
return manifests, nil
}
func (m mongoRepo) FindManifestMetaData(manifestname string) (manifest.Metadata, bool, error) {
opts := options.FindOne()
opts.Projection = bson.D{
bsonp.E{Key: "files", Value: 0},
}
filter := bson.D{
bsonp.E{Key: "name", Value: manifestname},
}
var result manifest.Metadata
if err := m.collection.FindOne(context.Background(), filter, opts).Decode(&result); err != nil {
if err == mongo.ErrNoDocuments {
return manifest.Metadata{}, false, nil
}
return manifest.Metadata{}, false, err
}
return result, true, nil
}
func (m mongoRepo) FindManifest(manifestname string) (manifest.Manifest, bool, error) {
filter := bson.D{
bsonp.E{Key: "name", Value: manifestname},
}
var result manifest.Manifest
if err := m.collection.FindOne(context.Background(), filter).Decode(&result); err != nil {
if err == mongo.ErrNoDocuments {
return manifest.Manifest{}, false, nil
}
return manifest.Manifest{}, false, err
}
return result, true, nil
}
# Sunrise API Static JSON
Sunrise manifest API, backed by static JSON files.
## Adding your Manifest
To add a manifest, create a json file with the _manifest.json suffix in the application directory.
## Environment Variables
Sunrise API will read the .env file in your application folder on startup and assign environment variables.
`PORT` Port to listen on for http requests, begin with ":"
`CERT_FILE` Path to Cert File, required for TLS
`KEY_FILE` Path to Key File, required for TLS
## Resource Paths
```
GET /manifest/{name}
Returns the entire manifest.
GET /manifests/metadata
Returns the metadata of all available manifests.
GET /manifest/{name}/metadata
Returns the metadata of the manifest.
```
\ No newline at end of file
package main
import (
"log"
"github.com/joho/godotenv"
"git.ourodev.com/community/sunrise-api/srv"
)
func main() {
if err := loadConfig(); err != nil {
log.Fatal(err)
}
log.Printf("Reading JSON Manifests")
repo, err := readJsonManifests()
if err != nil {
log.Fatal(err)
}
config.srvConfig.Repo = repo
if err := srv.ListenAndServe(config.srvConfig); err != nil {
log.Fatal(err)
}
}
var config struct {
srvConfig srv.Config
}
func loadConfig() error {
if err := godotenv.Load(); err != nil {
return err
}
config.srvConfig.Load()
return nil
}
\ No newline at end of file
package main
import (
"os"
"encoding/json"
"io/ioutil"
"path/filepath"
"strings"
"git.ourodev.com/community/sunrise-api/manifest"
)
type staticManifests []manifest.Manifest
func readJsonManifests() (staticManifests, error) {
manifests := staticManifests{}
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !strings.HasSuffix(info.Name(), "_manifest.json") {
return nil
}
var m manifest.Manifest
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
err = json.Unmarshal(b, &m)
if err != nil {
return err
}
manifests = append(manifests, m)
return nil
})
if err != nil {
return nil, err
}
return manifests, nil
}
func (sm staticManifests) scanManifests(name string) (manifest.Manifest, bool) {
for i := range sm {
if sm[i].Name == name {
return sm[i], true
}
}
return manifest.Manifest{}, false
}
func (sm staticManifests) FindAllManifestMetaData() ([]manifest.Metadata, error) {
manifests := make([]manifest.Metadata, len(sm))
for i := range sm {
manifests[i] = sm[i].Metadata
}
return manifests, nil
}
func (sm staticManifests) FindManifestMetaData(manifestname string) (manifest.Metadata, bool, error) {
m, ok := sm.scanManifests(manifestname)
return m.Metadata, ok, nil
}
func (sm staticManifests) FindManifest(manifestname string) (manifest.Manifest, bool, error) {
m, ok := sm.scanManifests(manifestname)
return m, ok, nil
}
module git.ourodev.com/community/sunrise-api
go 1.13
require (
github.com/archnoc/manifiesta v0.0.4
github.com/gorilla/mux v1.7.4
github.com/joho/godotenv v1.3.0
github.com/klauspost/compress v1.10.6 // indirect
go.mongodb.org/mongo-driver v1.3.3
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect
)
This diff is collapsed.
package manifest
type Manifest struct {
Metadata `bson:",inline"`
Files []File `json:"files" bson:"files"`
}
type Metadata struct {
Name string `json:"name" bson:"name"`
Schema string `json:"schema,omitempty" bson:"-"`
Version string `json:"version" bson:"version"`
Desc string `json:"desc" bson:"desc"`
WelcomeURL string `json:"welcome_url,omitempty" bson:"welcome_url,omitempty"`
SupportURL string `json:"support_url,omitempty" bson:"support_url,omitempty"`
BannerURL string `json:"banner_url,omitempty" bson:"banner_url,omitempty"`
LaunchOptions []LaunchOption `json:"launch_options" bson:"launch_options"`
}
type LaunchOption struct {
Name string `json:"name" bson:"name"`
LaunchPath string `json:"path" bson:"path"`
Env string `json:"env" bson:"env"`
Args string `json:"args" bson:"args"`
}
type File struct {
Path string `json:"path" bson:"path"`
Size int64 `json:"size" bson:"size"`
SHA256 string `json:"sha256" bson:"sha256"`
Sources []Source `json:"sources" bson:"sources"`
}
type Source struct {
URL string `json:"url" bson:"url"`
}
\ No newline at end of file
package srv
import (
"encoding/json"
"log"
"net/http"
"git.ourodev.com/community/sunrise-api/manifest"
"github.com/gorilla/mux"
)
const schema = "sunrise-v1.0"
type ManifestHandler struct {
ManifestRepository ManifestRepository
}
//ManifestRepostiory is the source of manifest data
type ManifestRepository interface {
FindAllManifestMetaData() ([]manifest.Metadata, error)
FindManifestMetaData(manifest string) (manifest.Metadata, bool, error)
FindManifest(manifest string) (manifest.Manifest, bool, error)
}
func (h *ManifestHandler) GetAllManifestMetaData(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
manifests, err := h.ManifestRepository.FindAllManifestMetaData()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error (/manifests) - %v\n", err)
return
}
for i := range manifests {
manifests[i].Schema = schema
}
b, _ := json.Marshal(&manifests)
w.Write(b)
}
func (h *ManifestHandler) GetManifestMetaData(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
vars := mux.Vars(r)
manifestmetadata, ok, err := h.ManifestRepository.FindManifestMetaData(vars["name"])
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error (/manifests/%v/metadata) - %v\n", vars["name"], err)
return
}
if ok == false {
w.WriteHeader(http.StatusNotFound)
return
}
manifestmetadata.Schema = schema
b, _ := json.Marshal(&manifestmetadata)
w.Write(b)
}
func (h *ManifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
vars := mux.Vars(r)
manifest, ok, err := h.ManifestRepository.FindManifest(vars["name"])
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error (/manifests/%v) - %v\n", vars["name"], err)
return
}
if ok == false {
w.WriteHeader(http.StatusNotFound)
return
}
manifest.Schema = schema
b, _ := json.Marshal(&manifest)
w.Write(b)
}
package srv
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/gorilla/mux"
)
func getRouter(h handler) *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/manifests/metadata", h.GetAllManifestMetaData).Methods("GET")
r.HandleFunc("/manifests/{name}", h.GetManifest).Methods("GET")
r.HandleFunc("/manifests/{name}/metadata", h.GetManifestMetaData).Methods("GET")
return r
}
type handler interface {
GetAllManifestMetaData(w http.ResponseWriter, r *http.Request)
GetManifest(w http.ResponseWriter, r *http.Request)
GetManifestMetaData(w http.ResponseWriter, r *http.Request)
}
type Config struct {
Addr string
Certfile string
Keyfile string
Repo ManifestRepository
}
func (c *Config) Load() {
c.Addr = os.Getenv("PORT")
c.Certfile = os.Getenv("CERT_FILE")
c.Keyfile = os.Getenv("KEY_FILE")
}
func ListenAndServe(cfg Config) error {
if cfg.Repo == nil {
return errors.New("Config Error: Repo Not Found")
}
h := &ManifestHandler{ManifestRepository: cfg.Repo}
r := getRouter(h)
if cfg.Addr == "" {
return errors.New("Config Error: Repo Not Found")
}
srv := &http.Server{
Addr: cfg.Addr,
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: r,
}
go func() {
if cfg.Certfile == "" || cfg.Keyfile == "" {
log.Printf("Manifest Server Available, Listening at %v", cfg.Addr)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
log.Printf("Manifest Server Available, Listening at %v", cfg.Addr)
if err := srv.ListenAndServeTLS(cfg.Certfile, cfg.Keyfile); err != nil {
log.Fatal(err)
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 15)
defer cancel()
srv.Shutdown(ctx)
log.Println("Shut Down Request Acknowledged")
return nil
}
\ No newline at end of file