12 changed files with 1024 additions and 264 deletions
@ -0,0 +1,32 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"crypto/sha256" |
|||
"encoding/hex" |
|||
) |
|||
|
|||
type Secret []byte |
|||
|
|||
func (r Secret) String() string { |
|||
return hex.EncodeToString(r) |
|||
} |
|||
|
|||
func (r Secret) Hashed() string { |
|||
hash := sha256.Sum256(r) |
|||
return hex.EncodeToString(hash[:]) |
|||
} |
|||
|
|||
func newRandomSecret(size int) (Secret, error) { |
|||
var r Secret = make([]byte, size) |
|||
_, err := rand.Read(r) |
|||
if err != nil { |
|||
return Secret{}, err |
|||
} |
|||
|
|||
return r, nil |
|||
} |
|||
|
|||
func secretFromHex(encoded string) (Secret, error) { |
|||
return hex.DecodeString(encoded) |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/magiconair/properties/assert" |
|||
) |
|||
|
|||
func TestRandomSecretLength(t *testing.T) { |
|||
secret, err := newRandomSecret(32) |
|||
if err != nil { |
|||
t.Errorf("newRandomSecret(): %s", err) |
|||
} |
|||
assert.Equal(t, len(secret), 32, "random secret is 32 bytes long") |
|||
} |
|||
|
|||
func TestSecretFromHex(t *testing.T) { |
|||
secretHex := "11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff" |
|||
secret, err := secretFromHex(secretHex) |
|||
if err != nil { |
|||
t.Errorf("secretFromHex(): %s", err) |
|||
} |
|||
assert.Equal(t, len(secret), 32, "secret value is 32 bytes long") |
|||
assert.Equal(t, secret.String(), secretHex, "Secret.String prints the secret value as hex") |
|||
} |
|||
|
|||
func TestSecretHashed(t *testing.T) { |
|||
secretHex := "2e6cf592c0c41e57643b915dd719e0ffb681fd5183c3498e8a9802730a03c3e6" |
|||
hashHex := "e4cb5359be709b6e35c48cfcfa2b661f576300000126dae2dd99d8949267c1c3" |
|||
secret, err := secretFromHex(secretHex) |
|||
if err != nil { |
|||
t.Errorf("secretFromHex(): %s", err) |
|||
} |
|||
assert.Equal(t, secret.Hashed(), hashHex, "Secret.Hashed prints the hashed value as hex") |
|||
} |
|||
@ -0,0 +1,308 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/gob" |
|||
"fmt" |
|||
"log" |
|||
"net/http" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/coreos/go-oidc" |
|||
"github.com/gorilla/sessions" |
|||
"golang.org/x/oauth2" |
|||
) |
|||
|
|||
type SecurityFrontend struct { |
|||
OpenId OpenIdSettings |
|||
Protected http.Handler |
|||
TokenGenerator *TokenGenerator |
|||
GlobalTokenValidity int |
|||
PerAlbumTokenValidity int |
|||
|
|||
store *sessions.CookieStore |
|||
oAuth2Config *oauth2.Config |
|||
oidcVerifier *oidc.IDTokenVerifier |
|||
} |
|||
|
|||
type SessionSettings struct { |
|||
AuthenticationKey []byte |
|||
EncryptionKey []byte |
|||
CookieMaxAge int |
|||
SecureCookie bool |
|||
} |
|||
|
|||
type OpenIdSettings struct { |
|||
DiscoveryUrl string |
|||
ClientID string |
|||
ClientSecret string |
|||
RedirectURL string |
|||
GSuiteDomain string |
|||
Scopes []string |
|||
} |
|||
|
|||
func init() { |
|||
gob.Register(&WebUser{}) |
|||
} |
|||
|
|||
func NewSecurityFrontend(openidSettings OpenIdSettings, sessionSettings SessionSettings, tokenGenerator *TokenGenerator) (*SecurityFrontend, error) { |
|||
var securityFrontend SecurityFrontend |
|||
provider, err := oidc.NewProvider(context.TODO(), openidSettings.DiscoveryUrl) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
securityFrontend.oAuth2Config = &oauth2.Config{ |
|||
ClientID: openidSettings.ClientID, |
|||
ClientSecret: openidSettings.ClientSecret, |
|||
RedirectURL: openidSettings.RedirectURL, |
|||
|
|||
// Discovery returns the OAuth2 endpoints.
|
|||
Endpoint: provider.Endpoint(), |
|||
|
|||
// "openid" is a required scope for OpenID Connect flows.
|
|||
Scopes: append(openidSettings.Scopes, oidc.ScopeOpenID), |
|||
} |
|||
securityFrontend.oidcVerifier = provider.Verifier(&oidc.Config{ClientID: openidSettings.ClientID}) |
|||
securityFrontend.store = sessions.NewCookieStore(sessionSettings.AuthenticationKey, sessionSettings.EncryptionKey) |
|||
securityFrontend.store.Options = &sessions.Options{ |
|||
Path: "/", |
|||
MaxAge: sessionSettings.CookieMaxAge, |
|||
HttpOnly: true, |
|||
Secure: sessionSettings.SecureCookie, |
|||
} |
|||
|
|||
securityFrontend.OpenId = openidSettings |
|||
securityFrontend.TokenGenerator = tokenGenerator |
|||
|
|||
return &securityFrontend, nil |
|||
} |
|||
|
|||
func (securityFrontend *SecurityFrontend) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|||
originalPath := r.URL.Path |
|||
if r.URL.Path == "/oauth/callback" { |
|||
if r.Method != "GET" { |
|||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|||
return |
|||
} |
|||
|
|||
securityFrontend.handleOidcCallback(w, r) |
|||
return |
|||
} |
|||
|
|||
head, tail := ShiftPath(r.URL.Path) |
|||
var user *WebUser |
|||
if head == "s" { |
|||
var ok bool |
|||
r.URL.Path = tail |
|||
user, ok = securityFrontend.handleTelegramTokenAuthentication(w, r) |
|||
if !ok { |
|||
return |
|||
} |
|||
} else if head == "album" { |
|||
var ok bool |
|||
user, ok = securityFrontend.handleOidcAuthentication(w, r) |
|||
if !ok { |
|||
return |
|||
} |
|||
} else { |
|||
user = &WebUser{} |
|||
} |
|||
|
|||
log.Printf("[%s] %s %s", user, r.Method, r.URL.Path) |
|||
|
|||
// Respect the user's choice about trailing slash
|
|||
if strings.HasSuffix(originalPath, "/") && !strings.HasSuffix(r.URL.Path, "/") { |
|||
r.URL.Path = r.URL.Path + "/" |
|||
} |
|||
|
|||
securityFrontend.Protected.ServeHTTP(w, r) |
|||
} |
|||
|
|||
func (securityFrontend *SecurityFrontend) handleOidcRedirect(w http.ResponseWriter, r *http.Request, session *sessions.Session, forcedTargetPath string) { |
|||
nonce, err := newRandomSecret(32) |
|||
if err != nil { |
|||
log.Printf("rand.Read: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
state, err := newRandomSecret(32) |
|||
if err != nil { |
|||
log.Printf("rand.Read: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
session.AddFlash(nonce.String()) |
|||
session.AddFlash(state.String()) |
|||
if forcedTargetPath != "" { |
|||
session.AddFlash(forcedTargetPath) |
|||
} else { |
|||
session.AddFlash(r.URL.Path) |
|||
} |
|||
|
|||
err = session.Save(r, w) |
|||
if err != nil { |
|||
log.Printf("Session.Save: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
http.Redirect(w, r, securityFrontend.oAuth2Config.AuthCodeURL(state.Hashed(), oidc.Nonce(nonce.Hashed())), http.StatusFound) |
|||
} |
|||
|
|||
func (securityFrontend *SecurityFrontend) handleOidcCallback(w http.ResponseWriter, r *http.Request) { |
|||
session, err := securityFrontend.store.Get(r, "oidc") |
|||
if err != nil { |
|||
log.Printf("session.Store.Get: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
// Retrieve the nonce and state from the session flashes
|
|||
nonceAndState := session.Flashes() |
|||
if len(nonceAndState) < 3 { // there may be more than two if the user performs multiple attempts
|
|||
log.Printf("session.Flashes: no (nonce,state,redirect_path) found in current session (len = %d)", len(nonceAndState)) |
|||
securityFrontend.handleOidcRedirect(w, r, session, "/") |
|||
return |
|||
} |
|||
|
|||
nonce, err := secretFromHex(nonceAndState[len(nonceAndState)-3].(string)) |
|||
if err != nil { |
|||
log.Printf("hex.DecodeString: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
state, err := secretFromHex(nonceAndState[len(nonceAndState)-2].(string)) |
|||
if err != nil { |
|||
log.Printf("hex.DecodeString: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
redirect_path := nonceAndState[len(nonceAndState)-1].(string) |
|||
if redirect_path == "" { |
|||
redirect_path = "/" |
|||
} |
|||
|
|||
if r.URL.Query().Get("state") != state.Hashed() { |
|||
log.Println("OIDC callback: state do not match") |
|||
http.Error(w, "state does not match", http.StatusBadRequest) |
|||
return |
|||
} |
|||
|
|||
oauth2Token, err := securityFrontend.oAuth2Config.Exchange(context.TODO(), r.URL.Query().Get("code")) |
|||
if err != nil { |
|||
log.Printf("oauth2.Config.Exchange: %s", err) |
|||
http.Error(w, "Invalid Authorization Code", http.StatusBadRequest) |
|||
return |
|||
} |
|||
|
|||
rawIDToken, ok := oauth2Token.Extra("id_token").(string) |
|||
if !ok { |
|||
log.Println("Token.Extra: No id_token field in oauth2 token") |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
user, err := securityFrontend.validateIdToken(rawIDToken, nonce.Hashed()) |
|||
if err != nil { |
|||
log.Printf("validateIdToken: %s", err) |
|||
//log.Printf("invalid id_token: %s", rawIDToken)
|
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
log.Printf("HTTP: user %s logged in", user.Username) |
|||
session.Values["user"] = &user |
|||
err = session.Save(r, w) |
|||
if err != nil { |
|||
log.Printf("Session.Save: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
http.Redirect(w, r, redirect_path, http.StatusFound) |
|||
} |
|||
|
|||
func (securityFrontend *SecurityFrontend) validateIdToken(rawIDToken string, nonce string) (WebUser, error) { |
|||
idToken, err := securityFrontend.oidcVerifier.Verify(context.TODO(), rawIDToken) |
|||
if err != nil { |
|||
return WebUser{}, fmt.Errorf("IDTokenVerifier.Verify: %s", err) |
|||
} |
|||
|
|||
if idToken.Nonce != nonce { |
|||
return WebUser{}, fmt.Errorf("nonces do not match in id_token") |
|||
} |
|||
|
|||
var claims struct { |
|||
Email string `json:"email"` |
|||
GSuiteDomain string `json:"hd"` |
|||
} |
|||
|
|||
err = idToken.Claims(&claims) |
|||
if err != nil { |
|||
return WebUser{}, fmt.Errorf("IdToken.Claims: %s", err) |
|||
} |
|||
|
|||
if securityFrontend.OpenId.GSuiteDomain != "" && securityFrontend.OpenId.GSuiteDomain != claims.GSuiteDomain { |
|||
return WebUser{}, fmt.Errorf("GSuite domain '%s' is not allowed", claims.GSuiteDomain) |
|||
} |
|||
|
|||
return WebUser{Username: claims.Email, Type: TypeOidcUser}, nil |
|||
} |
|||
|
|||
func (securityFrontend *SecurityFrontend) handleOidcAuthentication(w http.ResponseWriter, r *http.Request) (*WebUser, bool) { |
|||
session, err := securityFrontend.store.Get(r, "oidc") |
|||
if err != nil { |
|||
log.Printf("session.Store.Get: %s", err) |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return &WebUser{}, false |
|||
} |
|||
|
|||
u := session.Values["user"] |
|||
if u == nil { |
|||
securityFrontend.handleOidcRedirect(w, r, session, "") |
|||
return &WebUser{}, false |
|||
} |
|||
|
|||
user, ok := u.(*WebUser) |
|||
if !ok { |
|||
log.Println("Cannot cast session item 'user' as WebUser") |
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
|||
return &WebUser{}, false |
|||
} |
|||
|
|||
return user, true |
|||
} |
|||
|
|||
func (securityFrontend *SecurityFrontend) handleTelegramTokenAuthentication(w http.ResponseWriter, r *http.Request) (*WebUser, bool) { |
|||
var username, token string |
|||
username, r.URL.Path = ShiftPath(r.URL.Path) |
|||
token, r.URL.Path = ShiftPath(r.URL.Path) |
|||
var tail string |
|||
_, tail = ShiftPath(r.URL.Path) |
|||
album, _ := ShiftPath(tail) |
|||
|
|||
data := TokenData{ |
|||
Username: username, |
|||
Timestamp: time.Now(), |
|||
Entitlement: album, |
|||
} |
|||
ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.PerAlbumTokenValidity) |
|||
if err != nil { |
|||
http.Error(w, "Invalid Token", http.StatusBadRequest) |
|||
return nil, false |
|||
} |
|||
|
|||
if !ok { |
|||
data.Entitlement = "" |
|||
ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.GlobalTokenValidity) |
|||
if !ok || err != nil { |
|||
http.Error(w, "Invalid Token", http.StatusBadRequest) |
|||
return nil, false |
|||
} |
|||
} |
|||
|
|||
return &WebUser{Username: username, Type: TypeTelegramUser}, true |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"bytes" |
|||
"crypto" |
|||
"crypto/hmac" |
|||
"encoding/base64" |
|||
"encoding/binary" |
|||
"fmt" |
|||
"time" |
|||
) |
|||
|
|||
type TokenGenerator struct { |
|||
AuthenticationKey []byte |
|||
Algorithm crypto.Hash |
|||
} |
|||
|
|||
type TokenData struct { |
|||
Timestamp time.Time |
|||
Username string |
|||
Entitlement string |
|||
} |
|||
|
|||
func NewTokenGenerator(authenticationKey []byte, algorithm crypto.Hash) (*TokenGenerator, error) { |
|||
if !algorithm.Available() { |
|||
return nil, fmt.Errorf("Hash algorithm %d is not available", algorithm) |
|||
} |
|||
|
|||
return &TokenGenerator{AuthenticationKey: authenticationKey, Algorithm: algorithm}, nil |
|||
} |
|||
|
|||
func (g *TokenGenerator) NewToken(data TokenData) string { |
|||
// Fill a buffer with the token data
|
|||
buffer := getBufferFor(data) |
|||
|
|||
// Pass the token data to the hash function
|
|||
hasher := hmac.New(g.Algorithm.New, g.AuthenticationKey) |
|||
hasher.Write(buffer) |
|||
hash := hasher.Sum(nil) |
|||
|
|||
//fmt.Println(hex.EncodeToString(hash))
|
|||
|
|||
return base64.StdEncoding.EncodeToString(hash) |
|||
} |
|||
|
|||
func getBufferFor(data TokenData) []byte { |
|||
// Compute the number days since year 2000
|
|||
// Note: there is a one-off error if the token span across the end of a leap year
|
|||
var daysSinceY2K uint32 = uint32((data.Timestamp.Year()-2000)*365 + data.Timestamp.YearDay()) |
|||
//fmt.Printf("Days since Y2K = %d\n", daysSinceY2K)
|
|||
|
|||
// Pack the token data in a buffer
|
|||
// - number of days since epoch
|
|||
// - username that generated the token
|
|||
// - entitlement for the resulting token
|
|||
usernameBytes := []byte(data.Username) |
|||
entitlementBytes := []byte(data.Entitlement) |
|||
bufferLen := len(usernameBytes) + len(entitlementBytes) + 5 // 4 bytes for daysSinceEpoch + one '\0' separator
|
|||
var buffer []byte = make([]byte, bufferLen) |
|||
binary.LittleEndian.PutUint32(buffer, daysSinceY2K) |
|||
start, stop := 4, 4+len(usernameBytes) |
|||
copy(buffer[start:stop], usernameBytes) |
|||
start, stop = stop+1, stop+1+len(entitlementBytes) |
|||
copy(buffer[start:stop], entitlementBytes) |
|||
|
|||
//fmt.Println(hex.EncodeToString(buffer))
|
|||
|
|||
return buffer |
|||
} |
|||
|
|||
func (g *TokenGenerator) ValidateToken(data TokenData, token string, validity int) (bool, error) { |
|||
rawToken, err := base64.StdEncoding.DecodeString(token) |
|||
if err != nil { |
|||
return false, err |
|||
} |
|||
|
|||
hasher := hmac.New(g.Algorithm.New, g.AuthenticationKey) |
|||
for days := 0; days < validity; days = days + 1 { |
|||
attempt := data |
|||
attempt.Timestamp = data.Timestamp.Add(time.Hour * -24 * time.Duration(days)) |
|||
buffer := getBufferFor(attempt) |
|||
hasher.Reset() |
|||
hasher.Write(buffer) |
|||
hash := hasher.Sum(nil) |
|||
if bytes.Compare(hash, rawToken) == 0 { |
|||
return true, nil |
|||
} |
|||
} |
|||
|
|||
return false, nil |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"crypto" |
|||
"encoding/hex" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/magiconair/properties/assert" |
|||
) |
|||
|
|||
func TestTokenGenerator(t *testing.T) { |
|||
// export KEY="$(openssl rand -hex 32)"
|
|||
secretHex := "6b68b32607bae2c3d5e140efd8f4d5b6518fced3081fc6b28478b903ceef9aa3" |
|||
secret, err := hex.DecodeString(secretHex) |
|||
if err != nil { |
|||
t.Errorf("secretFromHex(): %s", err) |
|||
} |
|||
|
|||
g, err := NewTokenGenerator(secret, crypto.SHA256) |
|||
if err != nil { |
|||
t.Errorf("NewTokenGenerator(): %s", err) |
|||
} |
|||
now := time.Unix(1588703522, 0) // date +%s
|
|||
token := g.NewToken(TokenData{now, "nmasse", "read"}) |
|||
|
|||
// echo "000000: 021d 0000 6e6d 6173 7365 0072 6561 64" |xxd -r | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary |openssl base64
|
|||
expectedToken := "McChidYyEfEPkotTq08EW+eYHjd2QX+wlUzgGjOhWlY=" |
|||
assert.Equal(t, token, expectedToken, "expected a valid token") |
|||
|
|||
sixDaysLater := time.Unix(1589221922, 0) |
|||
ok, err := g.ValidateToken(TokenData{sixDaysLater, "nmasse", "read"}, expectedToken, 7) |
|||
if err != nil { |
|||
t.Errorf("ValidateToken(sixDaysLater): %s", err) |
|||
} |
|||
if !ok { |
|||
t.Errorf("ValidateToken(sixDaysLater): token is not valid") |
|||
} |
|||
|
|||
sevenDaysLater := time.Unix(1589308322, 0) |
|||
ok, err = g.ValidateToken(TokenData{sevenDaysLater, "nmasse", "read"}, expectedToken, 7) |
|||
if err != nil { |
|||
t.Errorf("ValidateToken(sevenDaysLater): %s", err) |
|||
} |
|||
if ok { |
|||
t.Errorf("ValidateToken(sevenDaysLater): token is valid") |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
package main |
|||
|
|||
import "fmt" |
|||
|
|||
type UserType int |
|||
|
|||
const ( |
|||
TypeAnonymous UserType = 0 |
|||
TypeTelegramUser UserType = 1 |
|||
TypeOidcUser UserType = 2 |
|||
) |
|||
|
|||
func (t UserType) String() string { |
|||
names := [...]string{ |
|||
"Anonymous", |
|||
"Telegram", |
|||
"OIDC", |
|||
} |
|||
|
|||
if t < TypeAnonymous || t > TypeOidcUser { |
|||
return "Unknown" |
|||
} |
|||
|
|||
return names[t] |
|||
} |
|||
|
|||
type WebUser struct { |
|||
Username string |
|||
Type UserType |
|||
} |
|||
|
|||
func (u WebUser) String() string { |
|||
if u.Type == TypeAnonymous { |
|||
return "Anonymous" |
|||
} |
|||
|
|||
return fmt.Sprintf("%s:%s", u.Type, u.Username) |
|||
} |
|||
@ -0,0 +1,205 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"html/template" |
|||
"io/ioutil" |
|||
"log" |
|||
"net/http" |
|||
"sort" |
|||
"strings" |
|||
"time" |
|||
|
|||
_ "github.com/nmasse-itix/Telegram-Photo-Album-Bot/statik" |
|||
) |
|||
|
|||
type WebInterface struct { |
|||
AlbumTemplate *template.Template |
|||
MediaTemplate *template.Template |
|||
IndexTemplate *template.Template |
|||
SiteName string |
|||
} |
|||
|
|||
func slurpFile(statikFS http.FileSystem, filename string) (string, error) { |
|||
fd, err := statikFS.Open(filename) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
defer fd.Close() |
|||
|
|||
content, err := ioutil.ReadAll(fd) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
|
|||
return string(content), nil |
|||
} |
|||
|
|||
func getTemplate(statikFS http.FileSystem, filename string, name string) (*template.Template, error) { |
|||
tmpl := template.New(name) |
|||
content, err := slurpFile(statikFS, filename) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
customFunctions := template.FuncMap{ |
|||
"video": func(files []string) string { |
|||
for _, file := range files { |
|||
if strings.HasSuffix(file, ".mp4") { |
|||
return file |
|||
} |
|||
} |
|||
return "" |
|||
}, |
|||
"photo": func(files []string) string { |
|||
for _, file := range files { |
|||
if strings.HasSuffix(file, ".jpeg") { |
|||
return file |
|||
} |
|||
} |
|||
return "" |
|||
}, |
|||
"short": func(t time.Time) string { |
|||
return t.Format("2006-01") |
|||
}, |
|||
} |
|||
|
|||
return tmpl.Funcs(customFunctions).Parse(content) |
|||
} |
|||
|
|||
func (bot *PhotoBot) HandleFileNotFound(w http.ResponseWriter, r *http.Request) { |
|||
http.Error(w, "File not found", http.StatusNotFound) |
|||
} |
|||
|
|||
func (bot *PhotoBot) HandleError(w http.ResponseWriter, r *http.Request) { |
|||
http.Error(w, "Internal server error", http.StatusInternalServerError) |
|||
} |
|||
|
|||
func (bot *PhotoBot) HandleDisplayAlbum(w http.ResponseWriter, r *http.Request, albumName string) { |
|||
if albumName == "latest" { |
|||
albumName = "" |
|||
} |
|||
|
|||
album, err := bot.MediaStore.GetAlbum(albumName, false) |
|||
if err != nil { |
|||
log.Printf("MediaStore.GetAlbum: %s", err) |
|||
bot.HandleError(w, r) |
|||
return |
|||
} |
|||
|
|||
err = bot.WebInterface.AlbumTemplate.Execute(w, album) |
|||
if err != nil { |
|||
log.Printf("Template.Execute: %s", err) |
|||
bot.HandleError(w, r) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func (bot *PhotoBot) HandleDisplayIndex(w http.ResponseWriter, r *http.Request) { |
|||
albums, err := bot.MediaStore.ListAlbums() |
|||
if err != nil { |
|||
log.Printf("MediaStore.ListAlbums: %s", err) |
|||
bot.HandleError(w, r) |
|||
return |
|||
} |
|||
|
|||
sort.Sort(sort.Reverse(albums)) |
|||
err = bot.WebInterface.IndexTemplate.Execute(w, struct { |
|||
Title string |
|||
Albums []Album |
|||
}{ |
|||
bot.WebInterface.SiteName, |
|||
albums, |
|||
}) |
|||
if err != nil { |
|||
log.Printf("Template.Execute: %s", err) |
|||
bot.HandleError(w, r) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func (bot *PhotoBot) HandleDisplayMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaId string) { |
|||
if albumName == "latest" { |
|||
albumName = "" |
|||
} |
|||
|
|||
media, err := bot.MediaStore.GetMedia(albumName, mediaId) |
|||
if err != nil { |
|||
log.Printf("MediaStore.GetMedia: %s", err) |
|||
bot.HandleError(w, r) |
|||
return |
|||
|
|||
} |
|||
|
|||
if media == nil { |
|||
bot.HandleFileNotFound(w, r) |
|||
return |
|||
} |
|||
|
|||
err = bot.WebInterface.MediaTemplate.Execute(w, media) |
|||
if err != nil { |
|||
log.Printf("Template.Execute: %s", err) |
|||
bot.HandleError(w, r) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func (bot *PhotoBot) HandleGetMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaFilename string) { |
|||
if albumName == "latest" { |
|||
albumName = "" |
|||
} |
|||
|
|||
fd, modtime, err := bot.MediaStore.OpenFile(albumName, mediaFilename) |
|||
if err != nil { |
|||
log.Printf("MediaStore.OpenFile: %s", err) |
|||
bot.HandleError(w, r) |
|||
return |
|||
} |
|||
defer fd.Close() |
|||
http.ServeContent(w, r, mediaFilename, modtime, fd) |
|||
} |
|||
|
|||
func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|||
originalPath := r.URL.Path |
|||
var resource string |
|||
resource, r.URL.Path = ShiftPath(r.URL.Path) |
|||
|
|||
if r.Method != "GET" { |
|||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|||
return |
|||
} |
|||
|
|||
if resource == "album" { |
|||
var albumName, kind, media string |
|||
albumName, r.URL.Path = ShiftPath(r.URL.Path) |
|||
kind, r.URL.Path = ShiftPath(r.URL.Path) |
|||
media, r.URL.Path = ShiftPath(r.URL.Path) |
|||
if albumName != "" { |
|||
if kind == "" && media == "" { |
|||
if !strings.HasSuffix(originalPath, "/") { |
|||
http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently) |
|||
return |
|||
} |
|||
bot.HandleDisplayAlbum(w, r, albumName) |
|||
return |
|||
} else if kind == "raw" && media != "" { |
|||
bot.HandleGetMedia(w, r, albumName, media) |
|||
return |
|||
} else if kind == "media" && media != "" { |
|||
bot.HandleDisplayMedia(w, r, albumName, media) |
|||
return |
|||
} |
|||
} else { |
|||
if !strings.HasSuffix(originalPath, "/") { |
|||
http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently) |
|||
return |
|||
} |
|||
bot.HandleDisplayIndex(w, r) |
|||
return |
|||
} |
|||
} else if resource == "" { |
|||
http.Redirect(w, r, "/album/", http.StatusMovedPermanently) |
|||
return |
|||
} |
|||
|
|||
bot.HandleFileNotFound(w, r) |
|||
} |
|||
Loading…
Reference in new issue