Commit 8aa2eda4 authored by Martin Radile's avatar Martin Radile

initial commit

parents
test.go
release
.vscode
stages:
- build
- docker_build
variables:
RELEASE_VERSION: "0.1.0"
build:
stage: build
image: golang:1.11.0
script:
- make clean
- make build-linux-amd64
- make build-linux-arm7
- make build-darwin
- make vet
- make lint
- make test-cover
artifacts:
paths:
- release/
docker_build:
dependencies:
- build
stage: docker_build
image: docker:stable
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
- docker build -t registry.gitlab.com/mradile/rssfeeder:$RELEASE_VERSION .
- docker push registry.gitlab.com/mradile/rssfeeder:$RELEASE_VERSION
- docker tag registry.gitlab.com/mradile/rssfeeder:$RELEASE_VERSION registry.gitlab.com/mradile/rssfeeder:latest
- docker push registry.gitlab.com/mradile/rssfeeder:latest
only:
- master
- tags
FROM alpine:3.8
VOLUME /rssfeederd/data
VOLUME /rssfeederd/config
WORKDIR /root/
COPY release/linux-amd64/rssfeederd rssfeederd
COPY release/linux-amd64/rssfeeder rssfeeder
ENTRYPOINT ["./rssfeederd", "run"]
VERSION := `cat VERSION`
PKG := gitlab.com/mradile/rssfeeder
SOURCES ?= $(shell find . -name "*.go" -type f)
all: clean build-linux-amd64 build-linux-arm7 build-darwin vet lint
.PHONY: build-linux-amd64
build-linux-amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -i -v -o release/linux-amd64/rssfeederd -ldflags="-X main.version=${VERSION}" cmd/rssfeederd/*.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -i -v -o release/linux-amd64/rssfeeder -ldflags="-X main.version=${VERSION}" cmd/rssfeeder/*.go
.PHONY: build-linux-arm7
build-linux-arm7:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -i -v -o release/linux-arm7/rssfeederd -ldflags="-X main.version=${VERSION}" cmd/rssfeederd/*.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -i -v -o release/linux-arm7/rssfeeder -ldflags="-X main.version=${VERSION}" cmd/rssfeeder/*.go
.PHONY: build-darwin
build-darwin:
CGO_ENABLED=0 GOOS=darwin go build -i -v -o release/darwin/rssfeederd -ldflags="-X main.version=${VERSION}" cmd/rssfeederd/*.go
CGO_ENABLED=0 GOOS=darwin go build -i -v -o release/darwin/rssfeeder -ldflags="-X main.version=${VERSION}" cmd/rssfeeder/*.go
vet:
go vet cmd/rssfeeder/*.go && go vet cmd/rssfeederd/*.go
lint:
@for file in ${SOURCES} ; do \
golint $$file ; \
done
.PHONY: test
test:
go test ./...
.PHONY: cover
cover:
go test -coverprofile=cover.out ./...
go tool cover -func=cover.out
.PHONY: cover-html
cover-html: cover
go tool cover -html=cover.out
.PHONY: clean
clean:
rm -rf release/*
rm -f cover.out
# RSSFeeder
RSSFeeder is server and cli client for creating and serving RSS feeds. The server stores entries in categories which can be accessed via an RSS feed. Data manipulation is secure by a login and RSS feeds have a random token in its URL.
The storage backend is an embedded boltdb database which stores its data in a file.
# Setup
To use this service, you must first start the server and then login. The default credentials for server and client are admin/admin (and should obviously be changed).
To start the server you must provide the run command. The only mandatory paramter is --secret. The secret is used for signing a JWT which is returned after successful login.
```BASH
rssfeederd run --secret someSecret
```
## Options
### General options
General options must be place before the run command
```
--help help for rssfeeder
--verbose verbose
--help, show help
--version, print the version
```
### Options for the run command
All options can also provided via ENV variables.
```
--port port, -p port port for service to listen on (default: 8000) [$PORT]
--db_path path path for to store database in (default: "/Users/mre") [$DB_PATH]
--uri uri uri where feeds will be accessible (default: "http://localhost:8000") [$SERVER_URI]
--secret secret secret for generatring jwt tokens [$SECRET]
--login login login for accessing the service (default: "admin") [$LOGIN]
--password password password for accessing the service (default: "admin") [$PASSWORD]
```
#Client
The client can login, add feed entries and print a list of all RSS feeds.
## Login
The client must login before it can add entries to the server or retrieve a feed list:
```BASH
rssfeeder login --login admin --password admin --server http://localhost:8000
```
The client retrieves a JWT and stores it in a config file in $HOME/.config/rssfeeder.config.json. The file should only be readable by the current user as the password is currently stored inside. This step has do be done once, the JWT is refreshed on demand.
## Adding entries
For adding entries to the default category simple call add with an URL.
```BASH
rssfeeder add http://example.org
```
Add an entry to a category:
```BASH
rssfeeder add --category test http://example.org
```
## Feed list
For getting a overview of all feeds
```BASH
rssfeeder feeds
```
0.1.0
\ No newline at end of file
package config
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"path"
homedir "github.com/mitchellh/go-homedir"
)
const configFilename = "rssfeeder.config.json"
//ClientConfig contains all option for talking to the server instance
type ClientConfig struct {
ServerURI string `json:"server_uri"`
Login string `json:"login"`
Password string `json:"password"`
Token string `json:"token"`
}
//Load loads config from disk
func Load() (*ClientConfig, error) {
configPath := getDefaultConfigPath()
filePath := getConfigFilePath(configPath)
configBytes, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
var config ClientConfig
if err := json.Unmarshal(configBytes, &config); err != nil {
return nil, err
}
return &config, nil
}
//Write writes config to disk
func Write(config *ClientConfig) error {
configBytes, err := json.Marshal(config)
if err != nil {
return err
}
configPath := getDefaultConfigPath()
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := os.Mkdir(configPath, 0755); err != nil {
return err
}
}
filePath := getConfigFilePath(configPath)
if err := ioutil.WriteFile(filePath, configBytes, 0644); err != nil {
return err
}
return nil
}
func getConfigFilePath(configPath string) string {
return path.Join(configPath, configFilename)
}
func getDefaultConfigPath() string {
home, err := homedir.Dir()
if err != nil {
log.Fatalf("could not determine config default path: %v", err)
}
return path.Join(home, "/.config")
}
package http
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
"gitlab.com/mradile/rssfeeder/client/config"
"gitlab.com/mradile/rssfeeder/rssfeeder"
)
//RestClient can talk to the server instance
type RestClient struct {
Config *config.ClientConfig
client *http.Client
}
//NewRestClient create a new RestClient instance
func NewRestClient(config *config.ClientConfig) *RestClient {
//fmt.Printf("Using server %s\n", config.ServerURI)
return &RestClient{
Config: config,
client: &http.Client{},
}
}
//Login logs the user with provided login and password in. If successful, a JWT is returned
func (rc *RestClient) Login(login, password string) (string, error) {
user := &rssfeeder.User{
Login: login,
Password: password,
}
userBytes, err := json.Marshal(user)
if err != nil {
return "", err
}
uri := rc.getRestURI("/login")
req, err := http.NewRequest("POST", uri, bytes.NewReader(userBytes))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
res, err := rc.client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
code := res.StatusCode
if code == http.StatusNoContent {
return "", fmt.Errorf("could not login %s", string(data))
}
var token rssfeeder.JwtToken
if err := json.Unmarshal(data, &token); err != nil {
return "", fmt.Errorf("could not unmarshal response '%s': %v", string(data), err)
}
return token.Token, nil
}
//AddEntry ads an entry
func (rc *RestClient) AddEntry(url, category string) error {
fe := &rssfeeder.FeedEntry{
Category: category,
URL: url,
Time: time.Now(),
}
feBytes, _ := json.Marshal(fe)
res, err := rc.makeUserRequest("PUT", "/api/v1/feedentry", bytes.NewReader(feBytes))
if err != nil {
return err
}
defer res.Body.Close()
code := res.StatusCode
if code == http.StatusNoContent {
return nil
}
data, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("Could not add url: status code: %d, response: %s", code, (string(data)))
}
//Feeds retrieves a list of all RSS feeds
func (rc *RestClient) Feeds() (*rssfeeder.FeedURLList, error) {
res, err := rc.makeUserRequest("GET", "/feeds", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
code := res.StatusCode
if code != http.StatusOK {
return nil, fmt.Errorf("could not fetch feed list: status code: %d, response: %s", code, (string(data)))
}
var list rssfeeder.FeedURLList
if err := json.Unmarshal(data, &list); err != nil {
return nil, fmt.Errorf("could not unmarshal response '%s': %v", string(data), err)
}
return &list, nil
}
func (rc *RestClient) makeUserRequest(method, url string, body io.Reader) (*http.Response, error) {
uri := rc.getRestURI(url)
req, err := http.NewRequest(method, uri, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+rc.Config.Token)
req.Header.Set("Content-Type", "application/json")
return rc.client.Do(req)
}
// func (rc *RestClient) responseToError(response *http.Response) error {
// data, err := ioutil.ReadAll(response.Body)
// if err != nil {
// return err
// }
// defer response.Body.Close()
// var em model.ErrorMessage
// if err := json.Unmarshal(data, &em); err != nil {
// return err
// }
// return fmt.Errorf(em.Error)
// }
func (rc *RestClient) getRestURI(url string) string {
return fmt.Sprintf("%s%s", rc.Config.ServerURI, url)
}
package main
import (
"fmt"
"log"
"os"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/urfave/cli"
"gitlab.com/mradile/rssfeeder/client/config"
"gitlab.com/mradile/rssfeeder/client/http"
)
const defaultCategory = "default"
var version string
var serverURI string
var category string
var userLogin string
var userPassword string
func main() {
cliApp := cli.NewApp()
cliApp.Name = "rssfeeder"
cliApp.Usage = "rf"
cliApp.Version = version
cliApp.Commands = []cli.Command{
{
Name: "add",
Usage: "add an entry",
Action: func(c *cli.Context) error {
url := c.Args().Get(0)
return add(url)
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "category, c",
Value: defaultCategory,
Usage: "`category` to add an entry to",
Destination: &category,
},
cli.StringFlag{
Name: "server, s",
Value: "http://localhost:8000",
Usage: "`server` uri",
Destination: &serverURI,
},
},
},
{
Name: "feeds",
Usage: "list all feeds",
Action: func(c *cli.Context) error {
return feeds()
},
},
{
Name: "login",
Usage: "login",
Action: func(c *cli.Context) error {
cc := &config.ClientConfig{
ServerURI: serverURI,
Login: userLogin,
Password: userPassword,
}
return login(cc)
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "login, l",
Value: "admin",
Usage: "`user` for logging in",
Destination: &userLogin,
},
cli.StringFlag{
Name: "password, p",
Value: "admin",
Usage: "`password` for logging in",
Destination: &userPassword,
},
cli.StringFlag{
Name: "server, s",
Value: "http://localhost:8000",
Usage: "`server` uri",
Destination: &serverURI,
},
},
},
}
err := cliApp.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
func initilalizeRestClient() (*http.RestClient, error) {
cc, err := config.Load()
if err != nil {
return nil, err
}
if expired, err := tokenExpired(cc.Token); err != nil {
return nil, err
} else if expired {
fmt.Printf("token expired, trying to login")
if err := login(cc); err != nil {
return nil, err
}
}
return http.NewRestClient(cc), nil
}
func feeds() error {
rc, err := initilalizeRestClient()
if err != nil {
return err
}
feeds, err := rc.Feeds()
if err != nil {
return err
}
for _, feed := range feeds.Feeds {
fmt.Println(feed)
}
return nil
}
func add(url string) error {
rc, err := initilalizeRestClient()
if err != nil {
return err
}
if err := rc.AddEntry(url, category); err != nil {
return err
}
fmt.Printf("added URL '%s' to '%s' category\n", url, category)
return nil
}
func login(cc *config.ClientConfig) error {
rc := http.NewRestClient(cc)
token, err := rc.Login(userLogin, userPassword)
if err != nil {
return err
}
if token == "" {
return fmt.Errorf("could not login, got empty token")
}
fmt.Printf("logged in as %s\n", userLogin)
cc.Token = token
if err := config.Write(cc); err != nil {
return err
}
return nil
}
func tokenExpired(tokenString string) (bool, error) {
token, err := jwt.ParseWithClaims(string(tokenString), &jwt.StandardClaims{}, nil)
if token == nil {
return false, fmt.Errorf("malformed token: %v", err)
}
claims := token.Claims.(*jwt.StandardClaims)
//fmt.Printf("%d %d ", claims.ExpiresAt, time.Now().Unix())
return claims.ExpiresAt-time.Now().Unix() < 0, nil
}
package main
import (
"os"
"gitlab.com/mradile/rssfeeder/server/http"
homedir "github.com/mitchellh/go-homedir"
"github.com/urfave/cli"
log "github.com/sirupsen/logrus"
"gitlab.com/mradile/rssfeeder/server/feeddelivering"
"gitlab.com/mradile/rssfeeder/server/feeding"
"gitlab.com/mradile/rssfeeder/server/feedlisting"
"gitlab.com/mradile/rssfeeder/server/storage"
)
var version string
var verbose bool
var debug bool
var port int
var dbPath string
var serverURI string
var secret string
var login string
var password string
func main() {
cliApp := cli.NewApp()
cliApp.Name = "rssfeederd"
cliApp.Usage = "rssfeederd run"
cliApp.Version = version
cliApp.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug",
Usage: "prints trace level log messages",
EnvVar: "DEBUG",
Destination: &debug,
},
cli.BoolFlag{
Name: "verbose",
Usage: "prints debug level log messages",
EnvVar: "VERBOSE",
Destination: &verbose,
},
}
cliApp.Commands = []cli.Command{
{
Name: "run",
Usage: "start the service",
Action: func(c *cli.Context) error {
setLogOptions()
initConfig()
stor := storage.NewStorage(dbPath)
defer stor.Close()
feeder := feeding.Service(stor)
feedLister := feedlisting.NewService(stor, serverURI)
feedDeliverer := feeddelivering.NewService(stor, serverURI)
if secret == "" {
log.Fatal("no secret provided")
}
config := &http.Config{
ServerURI: serverURI,
Secret: secret,
Login: login,
Password: password,
}
handler := http.NewHandler(config, feeder, feedLister, feedDeliverer)
server := http.NewServer(port, handler)
return server.Start()
},
Flags: []cli.Flag{
cli.IntFlag{
Name: "port, p",
Value: 8000,
Usage: "`port` for service to listen on",
EnvVar: "PORT",
Destination: &port,
},
cli.StringFlag{
Name: "db-path",
Value: GetDBDefaultPath(),
Usage: "`path` for to store database in",
EnvVar: "DB_PATH",
Destination: &dbPath,