Commit 4f948770 authored by Leonard Techel's avatar Leonard Techel
Browse files

Initial commit

parents
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
[*.{js,json}]
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
conf/app.json
conf/database.db
{
"ImportPath": "github.com/barnslig/mlmanager",
"GoVersion": "go1.7",
"GodepVersion": "v75",
"Packages": [
".",
"./mailinglistprovider",
"./remoteauthprovider"
],
"Deps": [
{
"ImportPath": "github.com/PuerkitoBio/goquery",
"Comment": "v1.0.1-2-g152b1a2",
"Rev": "152b1a2c8f5d0340f658bb656032a39b94e52958"
},
{
"ImportPath": "github.com/Unknwon/com",
"Rev": "28b053d5a2923b87ce8c5a08f3af779894a72758"
},
{
"ImportPath": "github.com/Unknwon/i18n",
"Rev": "3b48b6662051bed72d36efa3c1e897bdf96b2e37"
},
{
"ImportPath": "github.com/andybalholm/cascadia",
"Rev": "65919c611220063037b1db8eb334acaf17a6d8ea"
},
{
"ImportPath": "github.com/go-macaron/binding",
"Rev": "2502aaf4bce3a4e6451b4610847bfb8dffdb6266"
},
{
"ImportPath": "github.com/go-macaron/csrf",
"Rev": "715bca06a911dbd91c4f1d9ec65fd329664b5211"
},
{
"ImportPath": "github.com/go-macaron/i18n",
"Rev": "d2d3329f13b52314f3292c4cecb601fad13f02c8"
},
{
"ImportPath": "github.com/go-macaron/inject",
"Rev": "c5ab7bf3a307593cd44cb272d1a5beea473dd072"
},
{
"ImportPath": "github.com/go-macaron/session",
"Rev": "66031fcb37a0fff002a1f028eb0b3a815c78306b"
},
{
"ImportPath": "github.com/jinzhu/gorm",
"Rev": "c7b9acefb7d4570e0f46d94ce453467cc486d8fd"
},
{
"ImportPath": "github.com/jinzhu/inflection",
"Rev": "3272df6c21d04180007eb3349844c89a3856bc25"
},
{
"ImportPath": "github.com/lib/pq/hstore",
"Comment": "go1.0-cutoff-74-g8ad2b29",
"Rev": "8ad2b298cadd691a77015666a5372eae5dbfac8f"
},
{
"ImportPath": "github.com/mattn/go-sqlite3",
"Comment": "v1.2.0-2-gfba66eb",
"Rev": "fba66eb11643069e747022997e9be3b502b2c6fb"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "b2ed34f6fc8d65cc6a090fb87692ea6b1162fddd"
},
{
"ImportPath": "golang.org/x/net/html",
"Rev": "b2ed34f6fc8d65cc6a090fb87692ea6b1162fddd"
},
{
"ImportPath": "golang.org/x/net/html/atom",
"Rev": "b2ed34f6fc8d65cc6a090fb87692ea6b1162fddd"
},
{
"ImportPath": "gopkg.in/ini.v1",
"Comment": "v1.8.6",
"Rev": "afbd495e5aaea13597b5e14fe514ddeaa4d76fc3"
},
{
"ImportPath": "gopkg.in/macaron.v1",
"Rev": "564f398778297b0b083aaa070dda26e217f1e6b3"
}
]
}
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.
package main
import (
"crypto/tls"
"encoding/json"
"github.com/go-macaron/binding"
"github.com/go-macaron/csrf"
"github.com/go-macaron/i18n"
"github.com/go-macaron/session"
"github.com/jinzhu/gorm"
"gopkg.in/macaron.v1"
"html/template"
"net/http"
"fmt"
_ "github.com/mattn/go-sqlite3"
"github.com/barnslig/mlmanager/mailinglistprovider"
"github.com/barnslig/mlmanager/remoteauthprovider"
)
type DBConfig struct {
Dialect string `json:"dialect"`
Args string `json:"args"`
}
type ML struct {
Id string `json:"id"`
Title map[string]string `json:"title"`
Description map[string]string `json:"description"`
}
type MLConfig struct {
Provider string `json:"provider"`
Config json.RawMessage `json:"conf"`
Lists []ML `json:"lists"`
}
type AppConfig struct {
Env string `json:"env"`
HttpListen string `json:"http-listen"`
DB DBConfig `json:"database"`
OAuth2 remoteauthprovider.OAuth2Config `json:"oauth2"`
Mailinglists MLConfig `json:"mailinglists"`
}
type App struct {
cfg AppConfig
app *macaron.Macaron
}
func CreateApp(cfg AppConfig) (app *App) {
m := macaron.Classic()
app = &App{
cfg: cfg,
app: m,
}
if app.cfg.Env == "development" {
// Ignore broken certificates when doing HTTP client requests in development
http.DefaultClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
db, err := gorm.Open(cfg.DB.Dialect, cfg.DB.Args)
if err != nil {
panic(err)
}
//db.LogMode(true)
m.Use(i18n.I18n(i18n.Options{
Langs: []string{"en-US", "de-DE"},
Names: []string{"English", "Deutsch"},
}))
m.Use(macaron.Renderer(macaron.RenderOptions{
Funcs: []template.FuncMap{map[string]interface{}{
"URLFor": m.URLFor,
}},
}))
m.Use(session.Sessioner())
m.Use(csrf.Csrfer())
m.Use(func(ctx *macaron.Context, x csrf.CSRF) {
ctx.Resp.Header().Add("X-Frame-Options", "DENY")
ctx.Resp.Header().Add("X-Content-Type-Options", "nosniff")
ctx.Resp.Header().Add("X-XSS-Protection", "1; mode=block")
ctx.Data["csrf_token"] = x.GetToken()
})
m.Use(func(ctx *macaron.Context) {
ctx.Data["db"] = db
ctx.Data["app"] = app
})
m.Use(func(ctx *macaron.Context) {
var ml mailinglistprovider.MailinglistProvider
if app.cfg.Mailinglists.Provider == "mailman2" {
var cfg mailinglistprovider.Mailman2ProviderConfig
err := json.Unmarshal(app.cfg.Mailinglists.Config, &cfg)
if err != nil {
panic(err)
}
ml, err = mailinglistprovider.CreateMailman2Provider(cfg)
if err != nil {
panic(err)
}
}
ctx.MapTo(ml, (*mailinglistprovider.MailinglistProvider)(nil))
})
auth := remoteauthprovider.CreateOAuth2AuthProvider(m, db, app.cfg.OAuth2)
m.Group("/user", func() {
m.Combo("/login").
Get(auth.GetLogin).
Name("user/login")
m.Combo("/logout").
Post(IsAuthenticated, csrf.Validate, auth.PostLogout).
Name("user/logout")
})
m.Group("/mailinglists", func() {
m.Combo("/").
Get(IsAuthenticated, MailinglistsGet).
Post(IsAuthenticated, csrf.Validate, binding.BindIgnErr(MailinglistForm{}), MailinglistPost).
Name("mailinglists")
})
fmt.Println("up and running")
panic(http.ListenAndServe(app.cfg.HttpListen, m))
return
}
{
"env": "development",
"http-listen": "0.0.0.0:8081",
"database": {
"dialect": "sqlite3",
"args": "conf/database.db"
},
"oauth2": {
"base-url": "https://mailings.fsr.local",
"default-login-redirect": "/mailinglists",
"logout-redirect": "https://accounts.fsr.local/user",
"authorization-endpoint": "https://accounts.fsr.local/oauth2/authorize",
"token-endpoint": "https://accounts.fsr.local/oauth2/token",
"client-id": "12345",
"client-secret": "54321"
},
"mailinglists": {
"provider": "mailman2",
"conf": {
"admin-pw": "supersicher",
"list-url": "http://mailman.local/lists/admin/ucp/members/list?adminpw={{adminpw}}&findmember={{address}}",
"subscribe-url": "http://mailman.local/lists/admin/ucp/members/add?adminpw={{adminpw}}&subscribees={{address}}&subscribe_or_invite=0&send_welcome_msg_to_this_batch=0&notification_to_list_owner=0",
"unsubscribe-url": "http://mailman.local/lists/admin/ucp/members/remove?adminpw={{adminpw}}&unsubscribees={{address}}&send_unsub_ack_to_this_batch=0&send_unsub_notifications_to_list_owner=0"
},
"lists": [
{
"id": "ucp",
"title": {
"en-US": "User Control Panel development",
"de-DE": "Benutzerverwaltungs-Entwicklung"
},
"description": {
"en-US": "Let's talk about developing this thing of software",
"de-DE": "Lass uns über die Weiterentwicklung dieser Software reden!"
}
}
]
}
}
[user]
logout = Abmelden
[mailinglists]
main = Mailinglisten
explanation_heading = Mailinglisten
explanation_copy = Hier kannst du Mailinglisten der Fachschaft an deine Uni-Mailadresse abonnieren.
subscribed = Abonniert
subscribe = abonnieren
unsubscribe = deabonnieren
[user]
logout = Logout
[mailinglists]
main = Mailinglists
explanation_heading = Mailinglists
explanation_copy = Here you can subscribe to mailing lists of the Student Union to your university mail address.
subscribed = Subscribed
subscribe = subscribe
unsubscribe = unsubscribe
package main
import (
"github.com/go-macaron/session"
"gopkg.in/macaron.v1"
"fmt"
"net/url"
"github.com/barnslig/mlmanager/remoteauthprovider"
)
// Middleware to check if a session is set
// Should be used for every protected resource
func IsAuthenticated(ctx *macaron.Context, sess session.Store, f *session.Flash) {
if sess.Get("user") == nil {
f.Error(ctx.Tr("user.not_authenticated"))
// Include the current URL to point the user back after logging in
current_uri := url.QueryEscape(ctx.Req.URL.String())
redirect_uri := fmt.Sprintf("%s?redirect=%s", ctx.URLFor("user/login"), current_uri)
ctx.Redirect(redirect_uri)
return
}
// If a session is set, set the template variable User to the user's data
ctx.Data["User"] = sess.Get("user").(remoteauthprovider.User)
}
// Makes sure an URL is relative
// This function should be used to sanitize redirect URIs so users never
// get redirected to some scary mafia scamming website / ...
func cleanRedirectURL(src string) (dst string, err error) {
// If the supplied URL string is empty, an error is returned
if len(src) == 0 {
err = fmt.Errorf("Supplied URL is empty")
return
}
// Try to parse the given URL
redirect_uri, err := url.Parse(src)
if err != nil {
return
}
// Clean the URL / make it relative
redirect_uri.Scheme = ""
redirect_uri.User = nil
redirect_uri.Host = ""
dst = redirect_uri.String()
return
}
package mailinglistprovider
type MailinglistProvider interface {
IsSubscribed(email string) bool
Subscribe(email string)
Unsubscribe(email string)
}
package mailinglistprovider
import (
"github.com/PuerkitoBio/goquery"
"net/http"
"strings"
)
type Mailman2ProviderConfig struct {
AdminPW string `json:"admin-pw"`
// http://mailman.local/lists/admin/ucp/members/list?adminpw={{adminpw}}&findmember={{address}}
ListURL string `json:"list-url"`
// http://mailman.local/lists/admin/ucp/members/add?adminpw={{adminpw}}&subscribees={{address}}&subscribe_or_invite=0&send_welcome_msg_to_this_batch=0&notification_to_list_owner=0
SubscribeURL string `json:"subscribe-url"`
// http://mailman.local/lists/admin/ucp/members/remove?adminpw={{adminpw}}&unsubscribees={{address}}&send_unsub_ack_to_this_batch=0&send_unsub_notifications_to_list_owner=0
UnsubscribeURL string `json:"unsubscribe-url"`
}
type Mailman2Provider struct {
cfg Mailman2ProviderConfig
}
func CreateMailman2Provider(cfg Mailman2ProviderConfig) (ml *Mailman2Provider, err error) {
ml = &Mailman2Provider{cfg: cfg}
return
}
func (ml *Mailman2Provider) IsSubscribed(email string) (subscribed bool) {
url := ml.generateURL(ml.cfg.ListURL, email)
doc, err := goquery.NewDocument(url)
if err != nil {
subscribed = false
return
}
// thanks stallman
title := doc.Find("html > body > form > center > table > tbody > tr > td > center > em").Text()
subscribed = title == "1 members total"
return
}
func (ml *Mailman2Provider) Subscribe(email string) {
url := ml.generateURL(ml.cfg.SubscribeURL, email)
http.Get(url)
}
func (ml *Mailman2Provider) Unsubscribe(email string) {
url := ml.generateURL(ml.cfg.UnsubscribeURL, email)
http.Get(url)
}
func (ml *Mailman2Provider) generateURL(urlTmpl string, email string) (url string) {
url = urlTmpl
url = strings.Replace(url, "{{adminpw}}", ml.cfg.AdminPW, 1)
url = strings.Replace(url, "{{address}}", email, 1)
return
}
package main
import (
"github.com/go-macaron/i18n"
"github.com/go-macaron/session"
"gopkg.in/macaron.v1"
"github.com/barnslig/mlmanager/mailinglistprovider"
"github.com/barnslig/mlmanager/remoteauthprovider"
)
type MailinglistForm struct {
ListID string `binding:"Required"`
Subscribed bool `binding:"Required"`
}
type List struct {
Id string
Title string
Description string
Subscribed bool
}
func MailinglistsGet(ctx *macaron.Context, sess session.Store, ml mailinglistprovider.MailinglistProvider, locale i18n.Locale) {
app := ctx.Data["app"].(*App)
user := sess.Get("user").(remoteauthprovider.User)
var lists []List
for _, listcfg := range app.cfg.Mailinglists.Lists {
list := List{
Id: listcfg.Id,
Title: listcfg.Title[locale.Lang],
Description: listcfg.Description[locale.Lang],
Subscribed: ml.IsSubscribed(user.Mail),
}
lists = append(lists, list)
}
ctx.Data["Mailinglists"] = lists
ctx.HTML(200, "mailinglists/index")
}
func MailinglistPost(ctx *macaron.Context, sess session.Store, f *session.Flash, postData MailinglistForm, ml mailinglistprovider.MailinglistProvider) {
user := sess.Get("user").(remoteauthprovider.User)
if postData.Subscribed {
ml.Subscribe(user.Mail)
} else {
ml.Unsubscribe(user.Mail)
}
ctx.Redirect(ctx.URLFor("mailinglists"))
}
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"log"
)
var (
configPath = flag.String("c", "conf/app.json", "Config file path")
)
func parseConfig(filePath string) (cfg AppConfig, err error) {
file, err := ioutil.ReadFile(filePath)
if err != nil {
return
}
err = json.Unmarshal(file, &cfg)
if err != nil {
return
}
return
}
func main() {
flag.Parse()
cfg, err := parseConfig(*configPath)
if err != nil {
log.Fatal(err)
}
CreateApp(cfg)
}
package remoteauthprovider
import (
"encoding/json"
"fmt"
"github.com/go-macaron/session"
"github.com/jinzhu/gorm"
"gopkg.in/macaron.v1"
"net/http"
"net/url"
)
/* database model */
type OAuth2Grant struct {
gorm.Model
UserId string
Token string
}
/* https://tools.ietf.org/html/rfc6749#section-5.1 */
type OAuth2TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
User User `json:"user"`
}
/* https://tools.ietf.org/html/rfc6749#section-5.2 */
type OAuth2ErrorResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorUri string `json:"error_uri"`
}
type OAuth2Config struct {
BaseURL string `json:"base-url"`
DefaultLoginRedirect string `json:"default-login-redirect"`
LogoutRedirect string `json:"logout-redirect"`
AuthorizationEndpoint string `json:"authorization-endpoint"`
TokenEndpoint string `json:"token-endpoint"`
ClientID string `json:"client-id"`
ClientSecret string `json:"client-secret"`
}
type OAuth2AuthProvider struct {
cfg OAuth2Config
app *macaron.Macaron
db gorm.DB
}
func CreateOAuth2AuthProvider(m *macaron.Macaron, db gorm.DB, cfg OAuth2Config) (provider *OAuth2AuthProvider) {
provider = &OAuth2AuthProvider{
cfg: cfg,
app: m,
db: db,
}
db.AutoMigrate(&OAuth2Grant{})
m.Group("/oauth2", func() {
m.Combo("/redirect").
Get(provider.GetRedirect).
Name("oauth2/redirect")
})
return
}
func (provider *OAuth2AuthProvider) GetLogin(ctx *macaron.Context) {
// Build the redirect URL
redirect_uri, err := url.Parse(provider.cfg.BaseURL)
if err != nil {
panic(err)
}
redirect_uri.Path = ctx.URLFor("oauth2/redirect")
// Add the redirect uri if it exists
if ctx.Query("redirect") != "" {
redirect_query := redirect_uri.Query()
redirect_query.Add("redirect_uri", ctx.Query("redirect"))
redirect_uri.RawQuery = redirect_query.Encode()
}