aboutsummaryrefslogtreecommitdiffstats
path: root/icalproxy.go
blob: cf09e5d9e49d4170546f03d20c70b411458d8a52 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package main

import (
	"bytes"
	"log"
	"net/http"
	"net/url"
	"time"

	"github.com/emersion/go-ical"
	"golang.org/x/crypto/argon2"
)

func main() {
	cfg := loadConfig()
	printConfig(&cfg)

	handler := handler{ignore: cfg.Ignore, tokens: cfg.UserTokens}

	mux := http.ServeMux{}
	mux.HandleFunc("/", handler.handle)
	server := http.Server{
		Addr:              ":" + cfg.Port,
		Handler:           &mux,
		ReadTimeout:       5 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       30 * time.Second,
		ReadHeaderTimeout: 2 * time.Second,
	}

	log.Println("Listening on", cfg.Port)
	log.Fatal(server.ListenAndServe())
}

type handler struct {
	ignore ignoreRules
	tokens map[string]userToken
	calURL url.URL
}

func (h handler) makeTokenURL(token string) string {
	newURL := h.calURL
	query := newURL.Query()
	query.Set("token", token)
	newURL.RawQuery = query.Encode()
	return newURL.String()
}

func (h handler) handle(w http.ResponseWriter, r *http.Request) {
	brightspaceToken := r.URL.Query().Get("brightspace_token")
	proxyUserID := r.URL.Query().Get("proxy_user_id")
	proxyToken := r.URL.Query().Get("proxy_token")
	if !userOK(h.tokens, proxyUserID, []byte(proxyToken)) {
		http.Error(w, "Bad proxy_user_id or proxy_token", http.StatusUnauthorized)
		return
	}

	resp, err := http.Get(h.makeTokenURL(brightspaceToken))
	if err != nil {
		http.Error(w, "Error sending request to Brightspace", http.StatusInternalServerError)
		return
	}

	cal, err := ical.NewDecoder(resp.Body).Decode()
	if err != nil {
		http.Error(w, "Could not decode iCal", http.StatusInternalServerError)
		return
	}

	filter(h.ignore, cal.Component)

	if err = ical.NewEncoder(w).Encode(cal); err != nil {
		log.Println("Error writing calendar:", err)
	}
}

func filter(ignore ignoreRules, c *ical.Component) {
	j := 0
	for _, child := range c.Children {
		keep := true

		if child.Name == ical.CompEvent {
			if location := child.Props.Get(ical.PropLocation); location != nil {
				for _, locationRule := range ignore.LocationRegexes {
					if locationRule.MatchString(location.Value) {
						keep = false
					}
				}
			}
			if summary := child.Props.Get(ical.PropSummary); summary != nil {
				for _, summaryRule := range ignore.SummaryRegexes {
					if summaryRule.MatchString(summary.Value) {
						keep = false
					}
				}
			}
		}

		if keep {
			c.Children[j] = child
			j++
		}
	}
	c.Children = c.Children[:j]
}

func userOK(tokens map[string]userToken, id string, token []byte) bool {
	info, ok := tokens[id]
	if !ok {
		return false
	}
	return bytes.Compare(hash(token, info.Salt), info.Hash) == 0
}

func hash(token []byte, salt []byte) []byte {
	return argon2.IDKey(token, salt, 1, 64*1024, 4, 32)
}