Restructure device tree and remove soc server (#1060)

This commit is contained in:
andig 2021-05-21 19:18:14 +02:00 • committed by GitHub
parent 70da155c3d
commit 80177aa0ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 60 additions and 1450 deletions

View file

@ -102,13 +102,4 @@ image-update:
soc:
@echo Version: $(VERSION) $(BUILD_DATE)
go build -o evcc-soc $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/soc/client
server:
@echo Version: $(VERSION) $(BUILD_DATE)
go build -o soc-server $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/soc/server
publish-server:
GOOS=linux GOARCH=amd64 go build -o soc-server $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/soc/server
docker build -f soc/Dockerfile --platform linux/amd64 -t andig/evcc-cloud .
docker push andig/evcc-cloud
go build -o evcc-soc $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/cmd/soc

4
api/plugin.go Normal file
View file

@ -0,0 +1,4 @@
package api
// Custom meter/charger/vehicle type
const Custom = "custom"

View file

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/util"
)
@ -19,7 +18,7 @@ type Charger struct {
func init() {
registry.Add("default", NewConfigurableFromConfig)
registry.Add(internal.Custom, NewConfigurableFromConfig)
registry.Add(api.Custom, NewConfigurableFromConfig)
}
// NewConfigurableFromConfig creates a new configurable charger

View file

@ -7,8 +7,8 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/charger/easee"
"github.com/andig/evcc/core"
"github.com/andig/evcc/internal/charger/easee"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/request"
"github.com/andig/evcc/util/sponsor"

View file

@ -58,7 +58,7 @@ func init() {
registry.Add("evsewifi", NewEVSEWifiFromConfig)
}
//go:generate go run ../../cmd/tools/decorate.go -f decorateEVSE -b *EVSEWifi -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decorateEVSE -b *EVSEWifi -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
// NewEVSEWifiFromConfig creates a EVSEWifi charger from generic config
func NewEVSEWifiFromConfig(other map[string]interface{}) (api.Charger, error) {

View file

@ -12,7 +12,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger/fritzdect"
"github.com/andig/evcc/charger/fritzdect"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/request"
"golang.org/x/text/encoding/unicode"

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger/keba"
"github.com/andig/evcc/charger/keba"
"github.com/andig/evcc/util"
)

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger/nrgble"
"github.com/andig/evcc/charger/nrgble"
"github.com/andig/evcc/util"
"github.com/godbus/dbus/v5"
"github.com/lunixbochs/struc"

View file

@ -5,7 +5,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger/openwb"
"github.com/andig/evcc/charger/openwb"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/provider/mqtt"
"github.com/andig/evcc/util"

View file

@ -32,7 +32,7 @@ func init() {
registry.Add("phoenix-em-eth", NewPhoenixEMEthFromConfig)
}
//go:generate go run ../../cmd/tools/decorate.go -f decoratePhoenixEMEth -b *PhoenixEMEth -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decoratePhoenixEMEth -b *PhoenixEMEth -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
// NewPhoenixEMEthFromConfig creates a Phoenix charger from generic config
func NewPhoenixEMEthFromConfig(other map[string]interface{}) (api.Charger, error) {

View file

@ -33,7 +33,7 @@ func init() {
registry.Add("phoenix-ev-eth", NewPhoenixEVEthFromConfig)
}
//go:generate go run ../../cmd/tools/decorate.go -f decoratePhoenixEVEth -b *PhoenixEVEth -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decoratePhoenixEVEth -b *PhoenixEVEth -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
// NewPhoenixEVEthFromConfig creates a Phoenix charger from generic config
func NewPhoenixEVEthFromConfig(other map[string]interface{}) (api.Charger, error) {

View file

@ -7,7 +7,7 @@ import (
"strings"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger/tasmota"
"github.com/andig/evcc/charger/tasmota"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/request"
)

View file

@ -11,7 +11,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger/tplink"
"github.com/andig/evcc/charger/tplink"
"github.com/andig/evcc/util"
)

View file

@ -64,7 +64,7 @@ type Warp struct {
enabled bool // cache
}
//go:generate go run ../../cmd/tools/decorate.go -f decorateWarp -b *Warp -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decorateWarp -b *Warp -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)"
// NewWarp creates a new configurable charger
func NewWarp(mqttconf mqtt.Config, topic string, timeout time.Duration) (*Warp, error) {

View file

@ -5,12 +5,12 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger"
"github.com/andig/evcc/internal/meter"
"github.com/andig/evcc/internal/vehicle"
"github.com/andig/evcc/charger"
"github.com/andig/evcc/meter"
"github.com/andig/evcc/provider/mqtt"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
"github.com/andig/evcc/vehicle"
)
type config struct {

View file

@ -8,13 +8,13 @@ import (
"strconv"
"time"
"github.com/andig/evcc/api/proto/pb"
"github.com/andig/evcc/core"
"github.com/andig/evcc/hems"
"github.com/andig/evcc/provider/javascript"
"github.com/andig/evcc/provider/mqtt"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/cloud"
"github.com/andig/evcc/util/pipe"

View file

@ -10,8 +10,8 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/vehicle"
"github.com/andig/evcc/util"
"github.com/andig/evcc/vehicle"
)
func usage() {

View file

@ -4,7 +4,7 @@ import (
"sync"
"time"
"github.com/andig/evcc/internal/charger/keba"
"github.com/andig/evcc/charger/keba"
"github.com/andig/evcc/util"
)

View file

@ -7,7 +7,7 @@ import (
"sync"
"time"
"github.com/andig/evcc/internal/meter/sma"
"github.com/andig/evcc/meter/sma"
"github.com/andig/evcc/util"
)

5
go.mod
View file

@ -14,7 +14,6 @@ require (
github.com/containrrr/shoutrrr v0.4.4
github.com/deepmap/oapi-codegen v1.6.1 // indirect
github.com/denisbrodbeck/machineid v1.0.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dylanmei/iso8601 v0.1.0
github.com/eclipse/paho.mqtt.golang v1.3.4
github.com/fatih/structs v1.1.0
@ -61,8 +60,6 @@ require (
github.com/prometheus/common v0.23.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cobra v1.1.3
@ -72,7 +69,7 @@ require (
github.com/thoas/go-funk v0.8.0
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20210117183837-59dcc46d62d4
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210508051633-16afe75a6701
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c

5
go.sum
View file

@ -135,7 +135,6 @@ github.com/deepmap/oapi-codegen v1.6.1 h1:2BvsmRb6pogGNtr8Ann+esAbSKFXx2CZN18VpA
github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@ -630,10 +629,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa h1:jozR3igKlnYCj9IVHOVump59bp07oIRoLQ/CcjMYIUA=
github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

View file

@ -1,4 +0,0 @@
package internal
// Custom plugin type
const Custom = "custom"

View file

@ -5,17 +5,16 @@ import (
"fmt"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/util"
)
func init() {
registry.Add("default", NewConfigurableFromConfig)
registry.Add(internal.Custom, NewConfigurableFromConfig)
registry.Add(api.Custom, NewConfigurableFromConfig)
}
//go:generate go run ../../cmd/tools/decorate.go -f decorateMeter -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" -t "api.Battery,SoC,func() (float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decorateMeter -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" -t "api.Battery,SoC,func() (float64, error)"
// NewConfigurableFromConfig creates api.Meter from config
func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error) {

View file

@ -27,7 +27,7 @@ func init() {
registry.Add("modbus", NewModbusFromConfig)
}
//go:generate go run ../../cmd/tools/decorate.go -f decorateModbus -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,SoC,func() (float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decorateModbus -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,SoC,func() (float64, error)"
// NewModbusFromConfig creates api.Meter from config
func NewModbusFromConfig(other map[string]interface{}) (api.Meter, error) {

View file

@ -7,7 +7,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/charger/openwb"
"github.com/andig/evcc/charger/openwb"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/provider/mqtt"
"github.com/andig/evcc/util"

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/meter/sma"
"github.com/andig/evcc/meter/sma"
"github.com/andig/evcc/util"
)
@ -37,7 +37,7 @@ func init() {
registry.Add("sma", NewSMAFromConfig)
}
//go:generate go run ../../cmd/tools/decorate.go -f decorateSMA -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decorateSMA -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)"
// NewSMAFromConfig creates a SMA Meter from generic config
func NewSMAFromConfig(other map[string]interface{}) (api.Meter, error) {

View file

@ -3,7 +3,7 @@ package meter
import (
"testing"
"github.com/andig/evcc/internal/meter/sma"
"github.com/andig/evcc/meter/sma"
"github.com/andig/evcc/util"
)

View file

@ -9,7 +9,7 @@ import (
"strings"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/meter/powerwall"
"github.com/andig/evcc/meter/powerwall"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/request"
)
@ -26,7 +26,7 @@ func init() {
registry.Add("tesla", NewTeslaFromConfig)
}
//go:generate go run ../../cmd/tools/decorate.go -f decorateTesla -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,SoC,func() (float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decorateTesla -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,SoC,func() (float64, error)"
// NewTeslaFromConfig creates a Tesla Powerwall Meter from generic config
func NewTeslaFromConfig(other map[string]interface{}) (api.Meter, error) {

View file

@ -1,35 +0,0 @@
# STEP 1 build executable binary
# FROM golang:1.16-alpine as builder
# # Install git + SSL ca certificates.
# # Git is required for fetching the dependencies.
# # Ca-certificates is required to call HTTPS endpoints.
# RUN apk update && apk add --no-cache git ca-certificates tzdata alpine-sdk && update-ca-certificates
# WORKDIR /build
# # install go tools and cache modules
# COPY Makefile .
# COPY go.mod .
# COPY go.sum .
# RUN make install
# RUN go mod download
# # build
# COPY . .
# RUN make server
FROM alpine:3.13 as builder
RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates
# STEP 2 build a small image including module support
FROM alpine:3.13
ENV TZ=Europe/Berlin
# Import from builder
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY soc-server /usr/local/bin/soc-server
CMD [ "soc-server" ]

View file

@ -1,28 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIE1DCCArwCCQC/TbpAPhcOQTANBgkqhkiG9w0BAQsFADAsMQswCQYDVQQGEwJE
RTEdMBsGCSqGSIb3DQEJARYOY3B1aWRsZUBnbXguZGUwHhcNMjEwMzMxMDc1ODAz
WhcNMjIwMzMxMDc1ODAzWjAsMQswCQYDVQQGEwJERTEdMBsGCSqGSIb3DQEJARYO
Y3B1aWRsZUBnbXguZGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+
noDTXbidh/5RFfsLlMJbfMn4Vu91oJb4MM41RQM8UOr3SRU/ZzcoLfWJe2ePpEDp
ZL4piobs8EIjFcWU2C/8La0rygbcDDqUJkW/nViH0Drs5ctgJl/uYeiDLnZiSZO+
PBoOA0trXxqlJPxuEseJbFaLvVE81DsLBnltDPqhJZEUJPEUIjtfStOhQM0f5YoO
Greewk7P5LUkTAuTIz+xk6uKnC1YF89SKmdPMH26Rcy6q1IqhEQG3tjUliJZiKS9
eQlnp9gskqNGop9gEsmFIMnhlrTUsVU2q38zLDFcmNDbNkRvv4Tf+7jHo4tLs02m
/fMTikdMNCRMOcJKbdobcwXq4Ghvc85xeM4/8wIQHEDFbLzmxi/4ibFfJjOfQOe9
VpK+TbeSBTxBw42CH9V0TUOz/LELYkMrJ+zfwMCvTV4Eodmc0gjwiy9aIYdpZXKF
ueqGGljhVCq2XqkaSATFu5CsdPZrfUszLiAVqpuVTMjy3VQ9ICzz1Bd5ICBRFt6l
fDsvfB6SgmB9r1bC+yr/tqe/unzUPajL5mZwn7Jev0uOl6mBNmQpcIEne+9cKedf
vPCv/tUQpwpz6blGacEtTxuM+fWDk+CLpPnId/4PEnIQWRFE2aXQ4nrJSwu3O5+k
vuZNWety8MOSkuQr0toH//p3VNHOtT7L4JWSlDfTZQIDAQABMA0GCSqGSIb3DQEB
CwUAA4ICAQANoLbh4Di1A485ggnKfp9ykfVvg+NduGm+eqr27be033N6IJ7fuKmF
7Ki1E+sp0IJDMdyikbqoHdbroHu2chwp/1GzFxhx3Vo7kswS/ehSxWpHjhtrGjAF
tSDUFR9q4C+km2A3k1ZNyd9C1w+R9Lr16j9lBQoGWmUgFsRf8ED16FMMSK/1mCC0
1VSydYnYhKmPZxMByTozGTV97wihA0XqXtadbkQoyvcnEuarBvEX3mPA3effqcEG
rreeDp1yzYtQRW85ZASaqgF5CYKhe9NekurZ5Jd+2mQPYRWPFpIFsSHoOLqbfcN1
/S0Si6LZWq0Mi8cbSi/zq+Eh7Q0w+EXP6Goh0A7M4e1dKt+cfGQpeyeg3cF49Cda
RrTkSv2Sl2jbrShqAsG/HNhyBaI/gtLGi9tig9wAoV2zjGw1Ehs0GcwFq2g/f6Zv
W7rdZIBZT1RIQLczuwsEhv6cFrSM5OU2f5fuKnmeI6uvyz0jRqiMncYRjwI6LotB
sWmdTWi6xsctYirJ3Yip5Tqm01asyUuIeiT+eQ6I9CfHLiGBXEzxlK5PrFqfD7gK
LfB1gCN7SAlxKGepmFMwfF+fDrsurL2T2ePxaaNBLjRAzmpSpH0NedMd+eanuHXV
a/iOL2XmVis7iuFyk5M2XtLFfYBff+mxSPA8d9u5kCpoel0tx3iLmw==
-----END CERTIFICATE-----

View file

@ -1,14 +0,0 @@
package ca
import (
"bytes"
_ "embed"
)
//go:embed ca-cert.pem
var cert []byte
func PEM() []byte {
copy := bytes.NewBuffer(cert)
return copy.Bytes()
}

View file

@ -1,31 +0,0 @@
#!/bin/sh
set +ex
rm *.pem
# 1. Generate CA's private key and self-signed certificate
openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de"
echo "CA's self-signed certificate"
openssl x509 -in ca-cert.pem -noout -text
# 2. Generate web server's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de"
# 3. Use CA's private key to sign web server's CSR and get back the signed certificate
openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf
echo "Server's signed certificate"
openssl x509 -in server-cert.pem -noout -text
# 4. Generate client's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de"
# 5. Use CA's private key to sign client's CSR and get back the signed certificate
openssl x509 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile client-ext.cnf
echo "Client's signed certificate"
openssl x509 -in client-cert.pem -noout -text
rm *-req.pem

View file

@ -1,33 +0,0 @@
package server
import (
"bytes"
"crypto/tls"
_ "embed"
"encoding/base64"
"github.com/andig/evcc/util"
)
//go:embed server-cert.pem
var cert []byte
//--go:embed server-key.pem
var key []byte
func init() {
var err error
key, err = base64.StdEncoding.DecodeString(util.Getenv("SERVER_KEY"))
if err != nil {
panic(err)
}
}
func PEM() []byte {
copy := bytes.NewBuffer(cert)
return copy.Bytes()
}
func Certificate() (tls.Certificate, error) {
return tls.X509KeyPair(cert, key)
}

View file

@ -1,29 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIE+TCCAuGgAwIBAgIJALu7hrWUWtC8MA0GCSqGSIb3DQEBBQUAMCwxCzAJBgNV
BAYTAkRFMR0wGwYJKoZIhvcNAQkBFg5jcHVpZGxlQGdteC5kZTAeFw0yMTAzMzEw
NzU4MDVaFw0yMTA1MzAwNzU4MDVaMCwxCzAJBgNVBAYTAkRFMR0wGwYJKoZIhvcN
AQkBFg5jcHVpZGxlQGdteC5kZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBALzv8oy3yl+sH+7fezx6aDJWf77aCaZmBrQyupXmd8InJazRx8ScmpO+LJ7i
ZtVA1YuHbL2qZaV5SBk15UkRaVRRNqVitQmaRFYDrrAtDMosOhVNGOSorGnrlOqP
eu8LD/bWwZ57NqWE2z7ZTnqNNdgX1urcvzs7jmopKl4i71sKHAJ1jdzEYl+7l7r1
HdtKU4K6axQ2BBP6dHB9v0wOJQzfER76mSWe+hotCTdIiGnZhaIO5NoW31i3KkPi
277bLse5wGGbD1FvzLPaWf0HyruxARPs49kxuxnYtJ+KUDk5LXpDC6MM2QcUdV/8
HVkTadONW/gVFKWS7u21NrWH2342K0lz4FyNEwiQGmTCnyT4A9DQmdfWirnXsf4M
Jkag7fWqK/dEFIJK14RHZx+i53UBIoLu6mTT+MrHzdXRNewg9jlSwOYROG80DEMh
WIbTcYqUk9XPSUzzaeg2FuP7ZFEmpbsltsztaEQikd5PO9Dkl+Qzm5eiQMwNxJX5
bgb6eLQOI+HOtQHSWVTBXeM7Vf0JCfA7RAvKgRk4lK6JGijzGBSLuCLzXAPygiqb
59KgYf2EDaSmjzU+ZCLDR6l81SUDVnQj75obQt/TPBoVgwlCeOfFvLGGXY6tzJrp
8yH59MWpkRB4yQImTzmnoZ5klUjwuShi2bWLFiI0+FmXNuitAgMBAAGjHjAcMBoG
A1UdEQQTMBGCCSouZXZjYy5pb4cEAAAAADANBgkqhkiG9w0BAQUFAAOCAgEAlmh0
fR34BT1LTZcTZjFgk3a0/MaZ4zK8zdlwtQFHSlOh46t+TL+jps2bmLu9IlZxLF8E
lgSzLu5bWeP8H9HuBgji41RMtwvNUqSFUUMn08Dh/78lFISneqyJZ2CeRzwPYyYs
5xi7DBXitFbvtWj5GP3NQaDhsXAPoXtKXuqab3IE6eSwTJOxm5PJnvVdRTFSRQJW
W3qQFICQxMm6Oif+yCGZLgROj7sJWOOKJecevtAxYebOCWx5nxbXxAvOY3wiRerG
xsS3pT6Q5bVo5OxUjPeRb3Fe969JJEMqGaaGTGGtYP2IVkwarwzKBtcpwgG3NJYv
EYG/MEFkpBl6yYJRRB5D7EyPcBRcDHn53sJqnP+Y8Xq40tjfWhxZ9I+N8VWK9nwS
a9CKUHlwpB6aT/J1GNMiU2RXhQWgDv8qma6Vik7RH5ZmDMmtG18GERHTNdiGKk9b
v5xTphWMHl0NtcGPbPoc8PCrIVkb0frGAX5g6UVCHFfSkiUwLDV1zPhv/tLBa5hX
ZzW0wJzs6nDMj9NleyN1B1UY3mbyNKS2Ti9M9NBNEc+IEUdyj36RNmuvDmVNYTZ+
eYk28CFfkVT7XQjI1hIpBtu5JqBqic5Kzxg/Xwjmt1izpQBy8C7FFMb0Zs3uvrjo
FHk0eyJ/i830ZKCw8Ovx1fgMElZz8AZb/17aeRY=
-----END CERTIFICATE-----

View file

@ -1 +0,0 @@
subjectAltName=DNS:*.evcc.io,IP:0.0.0.0

View file

@ -1,88 +0,0 @@
package auth
import (
"context"
"errors"
"sync"
"time"
"github.com/andig/evcc/soc/server/auth/sponsor"
"github.com/andig/evcc/util"
"github.com/dgrijalva/jwt-go"
)
var (
Issuer = "evcc.io"
TokenExpiry = 365 // days
tokenSecret = util.Getenv("JWT_TOKEN_SECRET")
)
var (
mux sync.Mutex
updated time.Time
authorized = make(map[string]bool)
)
const updateInterval = 5 * time.Minute
type Claims struct {
UsernameDeprecated string `json:"username,omitempty"`
Username string `json:"usr,omitempty"`
SponsorExempt bool `json:"spe,omitempty"`
jwt.StandardClaims
}
func keyFunc(token *jwt.Token) (interface{}, error) {
return []byte(tokenSecret), nil
}
func AuthorizedToken(claims Claims) (string, error) {
claims.Issuer = Issuer
claims.IssuedAt = time.Now().Unix()
claims.ExpiresAt = time.Now().Add(24 * time.Hour * time.Duration(TokenExpiry)).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(tokenSecret))
}
func ParseToken(token string) (*Claims, error) {
jwt, err := jwt.ParseWithClaims(token, &Claims{}, keyFunc)
if err != nil {
return nil, err
}
claims, ok := jwt.Claims.(*Claims)
if !ok {
return nil, errors.New("claims invalid")
}
return claims, nil
}
func IsSponsor(login string) (bool, error) {
mux.Lock()
defer mux.Unlock()
if _, ok := authorized[login]; !ok {
if time.Since(updated) < updateInterval {
return false, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
all, err := sponsor.Get(ctx)
if err != nil {
return false, err
}
updated = time.Now()
for _, s := range all {
authorized[s.Login] = true
}
}
_, ok := authorized[login]
return ok, nil
}

View file

@ -1,79 +0,0 @@
package sponsor
import (
"context"
"github.com/andig/evcc/util"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
)
var client *githubv4.Client
func init() {
token := util.Getenv("GITHUB_TOKEN")
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(context.Background(), ts)
client = githubv4.NewClient(tc)
}
// Source: https://github.com/tj/sponsors-api/blob/master/server.go
// Sponsor model
type Sponsor struct {
Name string
Login string
AvatarURL string
}
// sponsorships query: https://docs.github.com/en/graphql/reference/objects
type sponsorships struct {
Viewer struct {
Login string
SponsorshipsAsMaintainer struct {
PageInfo struct {
EndCursor string
HasNextPage bool
}
Edges []struct {
Node struct {
Sponsor Sponsor
}
Cursor string
}
} `graphql:"sponsorshipsAsMaintainer(first: 100, after: $cursor, includePrivate: true)"`
}
}
// Get returns the list of sponsors
func Get(ctx context.Context) ([]Sponsor, error) {
var sponsors []Sponsor
var q sponsorships
var cursor string
for {
err := client.Query(ctx, &q, map[string]interface{}{
"cursor": githubv4.String(cursor),
})
if err != nil {
return nil, err
}
for _, edge := range q.Viewer.SponsorshipsAsMaintainer.Edges {
sponsor := edge.Node.Sponsor
sponsors = append(sponsors, sponsor)
}
if !q.Viewer.SponsorshipsAsMaintainer.PageInfo.HasNextPage {
break
}
cursor = q.Viewer.SponsorshipsAsMaintainer.PageInfo.EndCursor
}
return sponsors, nil
}

View file

@ -1,18 +0,0 @@
package main
import (
"log"
compile "github.com/andig/evcc/server"
"github.com/andig/evcc/soc/server/server"
"github.com/andig/evcc/soc/server/ui"
)
func main() {
log.Printf("soc-server %s (%s)", compile.Version, compile.Commit)
go ui.Run()
go server.Run()
select {}
}

View file

@ -1,47 +0,0 @@
package server
import (
"context"
"log"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/soc/server/auth"
)
type AuthServer struct {
pb.UnimplementedAuthServer
}
func (s *AuthServer) IsAuthorized(ctx context.Context, r *pb.AuthRequest) (*pb.AuthReply, error) {
authorized, _, claims, err := isAuthorized(r)
// track auth requests
log.Printf(claims.Subject+": %v", authorized)
res := &pb.AuthReply{Authorized: authorized}
if err == nil {
res.Subject = claims.Subject
}
return res, err
}
type tokenizer interface {
GetToken() string
}
func isAuthorized(r tokenizer) (bool, string, *auth.Claims, error) {
token := r.GetToken()
claims, err := auth.ParseToken(token)
if err != nil {
return false, token, claims, err
}
authorized := claims.SponsorExempt
if !authorized {
authorized, err = auth.IsSponsor(claims.Subject)
}
return authorized, token, claims, err
}

View file

@ -1,74 +0,0 @@
package server
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"github.com/andig/evcc/soc/cert/ca"
"github.com/andig/evcc/soc/cert/server"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/util"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var (
port = util.Getenv("GRPC_PORT", "8080")
tlsConfig *tls.Config
)
func init() {
var err error
if tlsConfig, err = loadTLSCredentials(); err != nil {
log.Fatalf("cannot load TLS credentials: %v", err)
}
registerMetrics()
}
func loadTLSCredentials() (*tls.Config, error) {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(ca.PEM()) {
return nil, fmt.Errorf("failed to add client CA's certificate")
}
// Load server's certificate and private key
serverCert, err := server.Certificate()
if err != nil {
return nil, err
}
// Create the credentials and return it
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.VerifyClientCertIfGiven,
ClientCAs: certPool,
}
return config, nil
}
func Run() {
log.Println("grpc:", ":"+port)
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
serverOptions := []grpc.ServerOption{}
if tlsConfig != nil {
serverOptions = append(serverOptions, grpc.Creds(credentials.NewTLS(tlsConfig)))
}
grpcServer := grpc.NewServer(serverOptions...)
pb.RegisterVehicleServer(grpcServer, &VehicleServer{
registry: make(map[string][]*VehicleContainer),
})
pb.RegisterAuthServer(grpcServer, &AuthServer{})
log.Fatal(grpcServer.Serve(listener))
}

View file

@ -1,35 +0,0 @@
package server
import (
"log"
"github.com/prometheus/client_golang/prometheus"
)
var (
promActiveVehicles *prometheus.GaugeVec
)
func registerMetrics() {
promActiveVehicles = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "soc",
Name: "active_vehicles",
}, []string{"token", "brand"})
if err := prometheus.Register(promActiveVehicles); err != nil {
log.Fatal(err)
}
}
func updateActiveVehiclesMetric(token, typ string, delta int) {
g, err := promActiveVehicles.GetMetricWith(prometheus.Labels{
"token": token,
"brand": typ,
})
if err != nil {
log.Println("get metrics:", err)
return
}
g.Add(float64(delta))
}

View file

@ -1,149 +0,0 @@
package server
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"log"
"sort"
"sync/atomic"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/vehicle"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/util/cloud"
)
var vehicleID int64
type VehicleContainer struct {
id int64
hash []byte
vehicle api.Vehicle
}
type VehicleServer struct {
registry map[string][]*VehicleContainer
pb.UnimplementedVehicleServer
}
type vehicler interface {
tokenizer
GetVehicleId() int64
}
func (s *VehicleServer) vehicle(r vehicler) (api.Vehicle, error) {
token := r.GetToken()
vehicles, ok := s.registry[token]
if !ok {
return nil, cloud.ErrVehicleNotAvailable
}
id := r.GetVehicleId()
for _, c := range vehicles {
if c.id == id {
return c.vehicle, nil
}
}
return nil, cloud.ErrVehicleNotAvailable
}
func stringMapToInterface(in map[string]string) map[string]interface{} {
res := make(map[string]interface{})
for k, v := range in {
res[k] = v
}
return res
}
func (s *VehicleServer) addVehicleToRegistry(token, typ string, config map[string]string, v api.Vehicle) int64 {
id := atomic.AddInt64(&vehicleID, 1)
// sort config keys
var keys []string
for k := range config {
keys = append(keys, k)
}
sort.Strings(keys)
// hash config
h := sha256.New()
_, _ = h.Write([]byte(typ))
for _, k := range keys {
_, _ = h.Write([]byte(k))
_, _ = h.Write([]byte(config[k]))
}
hash := h.Sum(nil)
// find vehicle by hash and update it
for _, c := range s.registry[token] {
if bytes.Equal(c.hash, hash) {
c.vehicle = v
c.id = id
return id
}
}
// register new vehicle
c := VehicleContainer{
id: id,
hash: hash,
vehicle: v,
}
s.registry[token] = append(s.registry[token], &c)
h.Reset()
_, _ = h.Write([]byte(token))
thash := fmt.Sprintf("%x", h.Sum(nil))
updateActiveVehiclesMetric(thash, typ, 1)
return id
}
func (s *VehicleServer) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error) {
authorized, token, claims, err := isAuthorized(r)
if err != nil {
return nil, err
}
if !authorized {
return nil, cloud.ErrNotAuthorized
}
typ := r.GetType()
config := r.GetConfig()
// track vehicle create
log.Println(claims.Subject+":", typ)
v, err := vehicle.NewFromConfig(typ, stringMapToInterface(config))
if err != nil {
return nil, err
}
id := s.addVehicleToRegistry(token, typ, config, v)
res := pb.NewReply{
VehicleId: id,
}
return &res, nil
}
func (s *VehicleServer) SoC(ctx context.Context, r *pb.SoCRequest) (*pb.SoCReply, error) {
v, err := s.vehicle(r)
if err != nil {
return nil, err
}
soc, err := v.SoC()
res := pb.SoCReply{
Soc: soc,
}
return &res, err
}

View file

@ -1,131 +0,0 @@
<!doctype html>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>evcc Sponsorship</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous">
</script>
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<style>
/* Custom default button */
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:focus {
color: #333;
text-shadow: none;
/* Prevent inheritance from `body` */
}
body {
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
}
.cover-container {
max-width: 42em;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, .5);
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255, 255, 255, .25);
}
.nav-masthead .nav-link+.nav-link {
margin-left: 1rem;
}
.nav-masthead .active {
color: #fff;
border-bottom-color: #fff;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body class="d-flex h-100 text-center text-white bg-dark">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="mb-auto">
<div>
<h3 class="float-md-start mb-0">evcc Sponsorship</h3>
<nav class="nav nav-masthead justify-content-center float-md-end">
<a class="nav-link active" aria-current="page" href="/">Home</a>
<a class="nav-link" href="/privacy">Impressum & Datenschutz</a>
</nav>
</div>
</header>
<main class="px-3">
{{if .Content}}
{{.Content}}
{{else}}
<h1>Willkommen</h1>
<p class="lead">
Die Freischaltung der Sonderfunktionen für evcc Sponsoren erfordert eine einmalige Anmeldung um evcc mit dem
Github Konto des evcc Sponsors zu verknüpfen. Mit der Verknüpfung wird das Registrierungstoken erzeugt.
</p>
<p class="lead">
Das Registrierungstoken gibt Zugang zu den folgenden Funktionen:
<p>Nutzung des evcc-soc Moduls mit OpenWB</p>
<p>Nutzung des evcc Easee Moduls</p>
</p>
<p class="lead">
<a href="/login" class="btn btn-lg btn-secondary fw-bold border-white bg-white">Anmelden</a>
</p>
{{end}}
</main>
<footer class="mt-auto text-white-50">
<p>Cover template by <a href="https://twitter.com/mdo" class="text-white">@mdo</a>.</p>
</footer>
</div>
</body>
</html>

View file

@ -1,220 +0,0 @@
<!doctype html>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Impressum und Datenschutzerklärung</title>
</head>
<body>
<h1>Impressum</h1>
<h2>Diensteanbieter</h2>
<p>Andreas Götz</p>
<p>Gubiner Str. 15</p>
<p>30880 Laatzen</p>
<h3>Kontaktmöglichkeiten</h3>
<p>E-Mail-Adresse: <a href="mailto:info@evcc.io">info@evcc.io</a></p>
<h1>Datenschutzerklärung</h1>
<h2>Einleitung</h2>
<p>Mit der folgenden Datenschutzerklärung möchten wir Sie darüber aufklären, welche Arten Ihrer personenbezogenen
Daten (nachfolgend auch kurz als "Daten“ bezeichnet) wir zu welchen Zwecken und in welchem Umfang verarbeiten. Die
Datenschutzerklärung gilt für alle von uns durchgeführten Verarbeitungen personenbezogener Daten, sowohl im
Rahmen der Erbringung unserer Leistungen als auch insbesondere auf unseren Webseiten, in mobilen Applikationen sowie
innerhalb externer Onlinepräsenzen, wie z.B. unserer Social-Media-Profile (nachfolgend zusammenfassend
bezeichnet als "Onlineangebot“).</p>
<p>Die verwendeten Begriffe sind nicht geschlechtsspezifisch.</p>
<p>Stand: 26. April 2021</p>
<h2>Inhaltsübersicht</h2>
<ul class="index">
<li><a class="index-link" href="#m14">Einleitung</a></li>
<li><a class="index-link" href="#m3">Verantwortlicher</a></li>
<li><a class="index-link" href="#mOverview">Übersicht der Verarbeitungen</a></li>
<li><a class="index-link" href="#m13">Maßgebliche Rechtsgrundlagen</a></li>
<li><a class="index-link" href="#m27">Sicherheitsmaßnahmen</a></li>
<li><a class="index-link" href="#m25">Übermittlung von personenbezogenen Daten</a></li>
<li><a class="index-link" href="#m24">Datenverarbeitung in Drittländern</a></li>
<li><a class="index-link" href="#m317">Geschäftliche Leistungen</a></li>
<li><a class="index-link" href="#m29">Bereitstellung des Onlineangebotes</a></li>
<li><a class="index-link" href="#m12">Löschung von Daten</a></li>
<li><a class="index-link" href="#m15">Änderung und Aktualisierung der Datenschutzerklärung</a></li>
<li><a class="index-link" href="#m10">Rechte der betroffenen Personen</a></li>
<li><a class="index-link" href="#m42">Begriffsdefinitionen</a></li>
</ul>
<h2 id="m3">Verantwortlicher</h2>
<p>Andreas Götz<br>Gubiner Str. 15<br>30880 Laatzen</p>
<p>E-Mail-Adresse: <a href="mailto:info@evcc.io">info@evcc.io</a>.</p>
<h2 id="mOverview">Übersicht der Verarbeitungen</h2>
<p>Die nachfolgende Übersicht fasst die Arten der verarbeiteten Daten und die Zwecke ihrer Verarbeitung zusammen und
verweist auf die betroffenen Personen.</p>
<h3>Arten der verarbeiteten Daten</h3>
<ul>
<li>Bestandsdaten (Github Profil und Sponsorship Level).</li>
<li>Meta-/Kommunikationsdaten (IP-Adressen, Hostnamen).</li>
<li>Vertragsdaten (Fahrzeugtyp und Zugangsdaten für Fahrzeug-APIs).</li>
</ul>
<h3>Kategorien betroffener Personen</h3>
<ul>
<li>Interessenten</li>
<li>Kunden</li>
<li>Nutzer</li>
</ul>
<h3>Zwecke der Verarbeitung</h3>
<ul>
<li>Bereitstellung unseres Onlineangebotes.</li>
<li>Verwaltung und Beantwortung von Anfragen.</li>
</ul>
<h3 id="m13">Maßgebliche Rechtsgrundlagen</h3>
<p>Im Folgenden erhalten Sie eine Übersicht der Rechtsgrundlagen der DSGVO, auf deren Basis wir personenbezogenen
Daten verarbeiten. Bitte nehmen Sie zur Kenntnis, dass neben den Regelungen der DSGVO nationale Datenschutzvorgaben
in Ihrem bzw. unserem Wohn- oder Sitzland gelten können. Sollten ferner im Einzelfall speziellere Rechtsgrundlagen
maßgeblich sein, teilen wir Ihnen diese in der Datenschutzerklärung mit.</p>
<ul>
<li><strong>Einwilligung (Art. 6 Abs. 1 S. 1 lit. a. DSGVO)</strong> - Die betroffene Person hat ihre
Einwilligung in die Verarbeitung der sie betreffenden personenbezogenen Daten für einen spezifischen Zweck oder
mehrere bestimmte Zwecke gegeben.</li>
<li><strong>Vertragserfüllung und vorvertragliche Anfragen (Art. 6 Abs. 1 S. 1 lit. b. DSGVO)</strong> - Die
Verarbeitung ist für die Erfüllung eines Vertrags, dessen Vertragspartei die betroffene Person ist, oder zur
Durchführung vorvertraglicher Maßnahmen erforderlich, die auf Anfrage der betroffenen Person erfolgen.</li>
<li><strong>Rechtliche Verpflichtung (Art. 6 Abs. 1 S. 1 lit. c. DSGVO)</strong> - Die Verarbeitung ist zur
Erfüllung einer rechtlichen Verpflichtung erforderlich, der der Verantwortliche unterliegt.</li>
<li><strong>Berechtigte Interessen (Art. 6 Abs. 1 S. 1 lit. f. DSGVO)</strong> - Die Verarbeitung ist zur
Wahrung der berechtigten Interessen des Verantwortlichen oder eines Dritten erforderlich, sofern nicht die
Interessen oder Grundrechte und Grundfreiheiten der betroffenen Person, die den Schutz personenbezogener Daten
erfordern, überwiegen.</li>
</ul>
<p><strong>Nationale Datenschutzregelungen in Deutschland</strong>: Zusätzlich zu den Datenschutzregelungen der
Datenschutz-Grundverordnung gelten nationale Regelungen zum Datenschutz in Deutschland. Hierzu gehört
insbesondere das Gesetz zum Schutz vor Missbrauch personenbezogener Daten bei der Datenverarbeitung
(Bundesdatenschutzgesetz – BDSG). Das BDSG enthält insbesondere Spezialregelungen zum Recht auf Auskunft, zum Recht
auf Löschung, zum Widerspruchsrecht, zur Verarbeitung besonderer Kategorien personenbezogener Daten, zur
Verarbeitung für andere Zwecke und zur Übermittlung sowie automatisierten Entscheidungsfindung im Einzelfall
einschließlich Profiling.
Des Weiteren regelt es die Datenverarbeitung für Zwecke des Beschäftigungsverhältnisses (§ 26 BDSG), insbesondere im
Hinblick auf die Begründung, Durchführung oder Beendigung von Beschäftigungsverhältnissen sowie die Einwilligung
von Beschäftigten. Ferner können Landesdatenschutzgesetze der einzelnen Bundesländer zur Anwendung gelangen.</p>
<h2 id="m27">Sicherheitsmaßnahmen</h2>
<p>Wir treffen nach Maßgabe der gesetzlichen Vorgaben unter Berücksichtigung des Stands der Technik, der
Implementierungskosten und der Art, des Umfangs, der Umstände und der Zwecke der Verarbeitung sowie der
unterschiedlichen Eintrittswahrscheinlichkeiten und des Ausmaßes der Bedrohung der Rechte und Freiheiten
natürlicher Personen geeignete technische und organisatorische Maßnahmen, um ein dem Risiko angemessenes
Schutzniveau zu gewährleisten.</p>
<p>Zu den Maßnahmen gehören insbesondere die Sicherung der Vertraulichkeit, Integrität und Verfügbarkeit von Daten
durch Kontrolle des physischen und elektronischen Zugangs zu den Daten als auch des sie betreffenden Zugriffs, der
Eingabe, der Weitergabe, der Sicherung der Verfügbarkeit und ihrer Trennung. Des Weiteren haben wir Verfahren
eingerichtet, die eine Wahrnehmung von Betroffenenrechten, die Löschung von Daten und Reaktionen auf die
Gefährdung der Daten gewährleisten. Ferner berücksichtigen wir den Schutz personenbezogener Daten bereits bei der
Entwicklung bzw. Auswahl von Hardware, Software sowie Verfahren entsprechend dem Prinzip des Datenschutzes, durch
Technikgestaltung und durch datenschutzfreundliche Voreinstellungen.</p>
<h2 id="m25">Übermittlung von personenbezogenen Daten</h2>
<p>Im Rahmen unserer Verarbeitung von personenbezogenen Daten kommt es vor, dass die Daten an andere Stellen,
Unternehmen, rechtlich selbstständige Organisationseinheiten oder Personen übermittelt oder sie ihnen gegenüber
offengelegt werden. Zu den Empfängern dieser Daten können z.B. mit IT-Aufgaben beauftragte Dienstleister oder
Anbieter von Diensten und Inhalten, die in eine Webseite eingebunden werden, gehören. In solchen Fall beachten
wir die gesetzlichen Vorgaben und schließen insbesondere entsprechende Verträge bzw. Vereinbarungen, die dem Schutz
Ihrer Daten dienen, mit den Empfängern Ihrer Daten ab.</p>
<h2 id="m24">Datenverarbeitung in Drittländern</h2>
<p>Sofern wir Daten in einem Drittland (d.h., außerhalb der Europäischen Union (EU), des Europäischen
Wirtschaftsraums (EWR)) verarbeiten oder die Verarbeitung im Rahmen der Inanspruchnahme von Diensten Dritter oder
der Offenlegung bzw. Übermittlung von Daten an andere Personen, Stellen oder Unternehmen stattfindet, erfolgt dies
nur im Einklang mit den gesetzlichen Vorgaben. </p>
<p>Vorbehaltlich ausdrücklicher Einwilligung oder vertraglich oder gesetzlich erforderlicher Übermittlung
verarbeiten oder lassen wir die Daten nur in Drittländern mit einem anerkannten Datenschutzniveau, vertraglichen
Verpflichtung durch sogenannte Standardschutzklauseln der EU-Kommission, beim Vorliegen von Zertifizierungen oder
verbindlicher internen Datenschutzvorschriften verarbeiten (Art. 44 bis 49 DSGVO, Informationsseite der
EU-Kommission: <a
href="https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_de"
target="_blank">https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_de</a>).
</p>
<h2 id="m29">Bereitstellung des Onlineangebotes</h2>
<p>Das Onlineangebote wird auf Servern von Prospect One (Karmelicka 5/5, Krakow, 31-133, Polen) betrieben. Wir
verweisen
auf die entsprechende <a href="https://appfleet.com/privacy-policy"> Datenschutzerklärung</a>
</p>
<p>
Wir verwenden Github für die Bereitstellung der Sponsoring Funktion sowie für die Generierung des Login Tokens.
Wir verweisen auf die entsprechende <a
href="https://docs.github.com/en/github/site-policy/github-privacy-statement"> Datenschutzerklärung</a>
</p>
<h2 id="m12">Löschung von Daten</h2>
<p>Die von uns verarbeiteten Daten werden nach Maßgabe der gesetzlichen Vorgaben gelöscht, sobald deren zur
Verarbeitung erlaubten Einwilligungen widerrufen werden oder sonstige Erlaubnisse entfallen (z.B., wenn der Zweck
der Verarbeitung dieser Daten entfallen ist oder sie für den Zweck nicht erforderlich sind).</p>
<p>Sofern die Daten nicht gelöscht werden, weil sie für andere und gesetzlich zulässige Zwecke erforderlich sind,
wird deren Verarbeitung auf diese Zwecke beschränkt. D.h., die Daten werden gesperrt und nicht für andere Zwecke
verarbeitet. Das gilt z.B. für Daten, die aus handels- oder steuerrechtlichen Gründen aufbewahrt werden müssen
oder deren Speicherung zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen oder zum Schutz der
Rechte einer anderen natürlichen oder juristischen Person erforderlich ist.</p>
<p>Unsere Datenschutzhinweise können ferner weitere Angaben zu der Aufbewahrung und Löschung von Daten beinhalten,
die für die jeweiligen Verarbeitungen vorrangig gelten.</p>
<h2 id="m15">Änderung und Aktualisierung der Datenschutzerklärung</h2>
<p>Wir bitten Sie, sich regelmäßig über den Inhalt unserer Datenschutzerklärung zu informieren. Wir passen die
Datenschutzerklärung an, sobald die Änderungen der von uns durchgeführten Datenverarbeitungen dies erforderlich
machen. Wir informieren Sie, sobald durch die Änderungen eine Mitwirkungshandlung Ihrerseits (z.B. Einwilligung)
oder eine sonstige individuelle Benachrichtigung erforderlich wird.</p>
<p>Sofern wir in dieser Datenschutzerklärung Adressen und Kontaktinformationen von Unternehmen und Organisationen
angeben, bitten wir zu beachten, dass die Adressen sich über die Zeit ändern können und bitten die Angaben vor
Kontaktaufnahme zu prüfen.</p>
<h2 id="m10">Rechte der betroffenen Personen</h2>
<p>Ihnen stehen als Betroffene nach der DSGVO verschiedene Rechte zu, die sich insbesondere aus Art. 15 bis 21 DSGVO
ergeben:</p>
<ul>
<li><strong>Widerspruchsrecht: Sie haben das Recht, aus Gründen, die sich aus Ihrer besonderen Situation
ergeben, jederzeit gegen die Verarbeitung der Sie betreffenden personenbezogenen Daten, die aufgrund von Art. 6
Abs. 1 lit. e oder f DSGVO erfolgt, Widerspruch einzulegen; dies gilt auch für ein auf diese Bestimmungen
gestütztes Profiling. Werden die Sie betreffenden personenbezogenen Daten verarbeitet, um Direktwerbung
zu betreiben, haben Sie das Recht, jederzeit Widerspruch gegen die Verarbeitung der Sie betreffenden
personenbezogenen Daten zum Zwecke derartiger Werbung einzulegen; dies gilt auch für das Profiling,
soweit es mit solcher Direktwerbung in Verbindung steht.</strong></li>
<li><strong>Widerrufsrecht bei Einwilligungen:</strong> Sie haben das Recht, erteilte Einwilligungen jederzeit
zu widerrufen.</li>
<li><strong>Auskunftsrecht:</strong> Sie haben das Recht, eine Bestätigung darüber zu verlangen, ob betreffende
Daten verarbeitet werden und auf Auskunft über diese Daten sowie auf weitere Informationen und Kopie der
Daten entsprechend den gesetzlichen Vorgaben.</li>
<li><strong>Recht auf Berichtigung:</strong> Sie haben entsprechend den gesetzlichen Vorgaben das Recht, die
Vervollständigung der Sie betreffenden Daten oder die Berichtigung der Sie betreffenden unrichtigen Daten zu
verlangen.</li>
<li><strong>Recht auf Löschung und Einschränkung der Verarbeitung:</strong> Sie haben nach Maßgabe der
gesetzlichen Vorgaben das Recht, zu verlangen, dass Sie betreffende Daten unverzüglich gelöscht werden, bzw.
alternativ nach Maßgabe der gesetzlichen Vorgaben eine Einschränkung der Verarbeitung der Daten zu verlangen.</li>
<li><strong>Recht auf Datenübertragbarkeit:</strong> Sie haben das Recht, Sie betreffende Daten, die Sie uns
bereitgestellt haben, nach Maßgabe der gesetzlichen Vorgaben in einem strukturierten, gängigen und
maschinenlesbaren Format zu erhalten oder deren Übermittlung an einen anderen Verantwortlichen zu fordern.
</li>
<li><strong>Beschwerde bei Aufsichtsbehörde:</strong> Sie haben unbeschadet eines anderweitigen
verwaltungsrechtlichen oder gerichtlichen Rechtsbehelfs das Recht auf Beschwerde bei einer Aufsichtsbehörde,
insbesondere in dem Mitgliedstaat ihres gewöhnlichen Aufenthaltsorts, ihres Arbeitsplatzes oder des Orts des
mutmaßlichen Verstoßes, wenn Sie der Ansicht sind, dass die Verarbeitung der Sie betreffenden
personenbezogenen Daten gegen die Vorgaben der DSGVO verstößt.</li>
</ul>
<h2 id="m42">Begriffsdefinitionen</h2>
<p>In diesem Abschnitt erhalten Sie eine Übersicht über die in dieser Datenschutzerklärung verwendeten
Begrifflichkeiten. Viele der Begriffe sind dem Gesetz entnommen und vor allem im Art. 4 DSGVO definiert. Die
gesetzlichen Definitionen sind verbindlich. Die nachfolgenden Erläuterungen sollen dagegen vor allem dem
Verständnis dienen. Die Begriffe sind alphabetisch sortiert.</p>
<ul class="glossary">
<li><strong>Personenbezogene Daten:</strong> "Personenbezogene Daten“ sind alle Informationen, die sich auf eine
identifizierte oder identifizierbare natürliche Person (im Folgenden "betroffene Person“) beziehen; als
identifizierbar wird eine natürliche Person angesehen, die direkt oder indirekt, insbesondere mittels
Zuordnung zu einer Kennung wie einem Namen, zu einer Kennnummer, zu Standortdaten, zu einer Online-Kennung (z.B.
Cookie) oder zu einem oder mehreren besonderen Merkmalen identifiziert werden kann, die Ausdruck der physischen,
physiologischen, genetischen, psychischen, wirtschaftlichen, kulturellen oder sozialen Identität dieser
natürlichen Person sind. </li>
<li><strong>Verantwortlicher:</strong> Als "Verantwortlicher“ wird die natürliche oder juristische Person,
Behörde, Einrichtung oder andere Stelle, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der
Verarbeitung von personenbezogenen Daten entscheidet, bezeichnet. </li>
<li><strong>Verarbeitung:</strong> "Verarbeitung" ist jeder mit oder ohne Hilfe automatisierter Verfahren
ausgeführte Vorgang oder jede solche Vorgangsreihe im Zusammenhang mit personenbezogenen Daten. Der Begriff
reicht weit und umfasst praktisch jeden Umgang mit Daten, sei es das Erheben, das Auswerten, das Speichern,
das Übermitteln oder das Löschen. </li>
</ul>
<p class="seal"><a href="https://datenschutz-generator.de/?l=de"
title="Rechtstext von Dr. Schwenke - für weitere Informationen bitte anklicken." target="_blank"
rel="noopener noreferrer nofollow">Erstellt mit kostenlosem Datenschutz-Generator.de von Dr. Thomas
Schwenke</a>
</p>
</body>
</html>

View file

@ -1,297 +0,0 @@
package ui
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"math/rand"
"net/http"
"net/http/pprof"
"os"
"sort"
"strings"
"time"
"github.com/andig/evcc/internal/vehicle"
"github.com/andig/evcc/soc/server/auth"
"github.com/andig/evcc/util"
"github.com/dgrijalva/jwt-go"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/thoas/go-funk"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
//go:embed index.html
var indexHtml string
//go:embed privacy.html
var privacyHtml string
var (
// login ui and callback
redirectURL = util.Getenv("REDIRECT_URL")
// SSL termination
sslCertDir = util.Getenv("SSL_CERT_DIR", "certs")
// github oauth application
clientID = util.Getenv("GITHUB_CLIENT_ID")
clientSecret = util.Getenv("GITHUB_CLIENT_SECRET")
)
var (
indexTpl, privacyTpl *template.Template
oauthState = randomString()
oauthConfig *oauth2.Config
)
const (
ProfileURL = "https://api.github.com/user"
EmailURL = "https://api.github.com/user/emails"
)
type User struct {
ID int `json:"id"`
Email string `json:"email"`
Bio string `json:"bio"`
Name string `json:"name"`
Login string `json:"login"`
Picture string `json:"avatar_url"`
Location string `json:"location"`
}
func init() {
clientID = util.Getenv("GITHUB_CLIENT_ID")
clientSecret = util.Getenv("GITHUB_CLIENT_SECRET")
oauthConfig = &oauth2.Config{
RedirectURL: redirectURL,
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{},
Endpoint: github.Endpoint,
}
indexTpl = template.Must(template.New("index").Parse(indexHtml))
privacyTpl = template.Must(template.New("privacy").Parse(privacyHtml))
}
func randomString() string {
rand.Seed(time.Now().UnixNano())
chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
length := 16
var b strings.Builder
for i := 0; i < length; i++ {
b.WriteRune(chars[rand.Intn(len(chars))])
}
return b.String()
}
func handleMain(w http.ResponseWriter, r *http.Request) {
_ = indexTpl.Execute(w, nil)
}
func handlePrivacy(w http.ResponseWriter, r *http.Request) {
_ = privacyTpl.Execute(w, nil)
}
func templateError(w http.ResponseWriter, r *http.Request, err string) {
_ = indexTpl.Execute(w, map[string]interface{}{
"Content": template.HTML(`
<h1>Fehler</h1>
<p class="lead">` + err + `</p>`),
})
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
url := oauthConfig.AuthCodeURL(oauthState)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func handleCallback(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("error") != "" {
err := r.URL.Query().Get("error_description")
templateError(w, r, err)
return
}
user, err := getUserInfo(r.FormValue("state"), r.FormValue("code"))
if err != nil {
templateError(w, r, err.Error())
return
}
authorized, err := auth.IsSponsor(user.Login)
if err != nil {
templateError(w, r, err.Error())
return
}
authMap := map[bool]string{false: "rejected", true: "authorized"}
log.Println(user.Login+":", authMap[authorized])
if !authorized {
_ = indexTpl.Execute(w, map[string]interface{}{
"Content": template.HTML(`
<h1>Inaktiv</h1>
<p class="lead">Github Sponsorship für evcc ist inaktiv. Um erweiterte Funktionen zu nutzen, ist es notwendig, evcc zu unterstützen.</p>
<p class="lead">evcc bei Github <a href="https://github.com/sponsors/andig" class="text-warning">unterstützen</a>.</p>`),
})
return
}
claims := auth.Claims{
Username: user.Name,
StandardClaims: jwt.StandardClaims{
Subject: user.Login,
},
}
jwt, err := auth.AuthorizedToken(claims)
if err != nil {
templateError(w, r, err.Error())
return
}
_ = indexTpl.Execute(w, map[string]interface{}{
"Content": template.HTML(fmt.Sprintf(`
<h1>Aktiv</h1>
<p class="lead">Github Sponsorship für evcc ist aktiv. Das folgende Registrierungstoken kann für den Zugriff auf evcc genutzt werden.</p>
<p class="lead">Der Code ist %d Tage gültig und kann jederzeit neu erzeugt werden.</p>
<p class="lead"><code>`+jwt+`</code></p>`, auth.TokenExpiry),
)})
}
func getUserInfo(state string, code string) (*User, error) {
if state != oauthState {
return nil, fmt.Errorf("invalid oauth state")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
token, err := oauthConfig.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("code exchange failed: %s", err.Error())
}
var user User
req, err := http.NewRequest("GET", ProfileURL, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
response, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode)
}
bits, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(bits, &user)
if err != nil {
return nil, err
}
return &user, nil
}
var typeFilter = []string{"custom", "cloud", "niu", "tesla"}
var typeMap = map[string]string{
"bmw": "BMW",
"citroen": "Citroën",
"skoda": "Škoda",
"enyaq": "Škoda Enyaq",
"vw": "VW",
"id": "VW ID",
}
type typeDesc struct {
ID string `json:"id"`
Name string `json:"name"`
}
func handleVehicles(w http.ResponseWriter, r *http.Request) {
types := make([]typeDesc, 0)
for _, id := range vehicle.Types() {
if funk.ContainsString(typeFilter, id) {
continue
}
vt := typeDesc{ID: id, Name: strings.Title(id)}
if name, ok := typeMap[id]; ok {
vt.Name = name
}
types = append(types, vt)
}
sort.Slice(types, func(i, j int) bool {
return strings.Compare(types[i].Name, types[j].Name) < 0
})
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
_ = json.NewEncoder(w).Encode(types)
}
func Run() {
mux := &http.ServeMux{}
mux.HandleFunc("/", handleMain)
mux.HandleFunc("/privacy", handlePrivacy)
mux.HandleFunc("/login", handleLogin)
mux.HandleFunc("/callback", handleCallback)
mux.HandleFunc("/api/vehicles", handleVehicles)
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
// mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
// mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
// mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
s := &http.Server{
Addr: ":http",
Handler: mux,
}
if sslHosts := strings.TrimSpace(os.Getenv("SSL_HOSTS")); sslHosts != "" {
log.Println("sslHosts:", sslHosts)
log.Println("sslCertDir:", sslCertDir)
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(strings.Split(sslHosts, " ")...),
Cache: autocert.DirCache(sslCertDir),
}
go func() {
log.Fatal(http.ListenAndServe(":http", certManager.HTTPHandler(nil)))
}()
s.Addr = ":https"
s.TLSConfig = certManager.TLSConfig()
log.Fatal(s.ListenAndServeTLS("", ""))
} else {
log.Fatal(s.ListenAndServe())
}
}

View file

@ -1,28 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIE1DCCArwCCQC/TbpAPhcOQTANBgkqhkiG9w0BAQsFADAsMQswCQYDVQQGEwJE
RTEdMBsGCSqGSIb3DQEJARYOY3B1aWRsZUBnbXguZGUwHhcNMjEwMzMxMDc1ODAz
WhcNMjIwMzMxMDc1ODAzWjAsMQswCQYDVQQGEwJERTEdMBsGCSqGSIb3DQEJARYO
Y3B1aWRsZUBnbXguZGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+
noDTXbidh/5RFfsLlMJbfMn4Vu91oJb4MM41RQM8UOr3SRU/ZzcoLfWJe2ePpEDp
ZL4piobs8EIjFcWU2C/8La0rygbcDDqUJkW/nViH0Drs5ctgJl/uYeiDLnZiSZO+
PBoOA0trXxqlJPxuEseJbFaLvVE81DsLBnltDPqhJZEUJPEUIjtfStOhQM0f5YoO
Greewk7P5LUkTAuTIz+xk6uKnC1YF89SKmdPMH26Rcy6q1IqhEQG3tjUliJZiKS9
eQlnp9gskqNGop9gEsmFIMnhlrTUsVU2q38zLDFcmNDbNkRvv4Tf+7jHo4tLs02m
/fMTikdMNCRMOcJKbdobcwXq4Ghvc85xeM4/8wIQHEDFbLzmxi/4ibFfJjOfQOe9
VpK+TbeSBTxBw42CH9V0TUOz/LELYkMrJ+zfwMCvTV4Eodmc0gjwiy9aIYdpZXKF
ueqGGljhVCq2XqkaSATFu5CsdPZrfUszLiAVqpuVTMjy3VQ9ICzz1Bd5ICBRFt6l
fDsvfB6SgmB9r1bC+yr/tqe/unzUPajL5mZwn7Jev0uOl6mBNmQpcIEne+9cKedf
vPCv/tUQpwpz6blGacEtTxuM+fWDk+CLpPnId/4PEnIQWRFE2aXQ4nrJSwu3O5+k
vuZNWety8MOSkuQr0toH//p3VNHOtT7L4JWSlDfTZQIDAQABMA0GCSqGSIb3DQEB
CwUAA4ICAQANoLbh4Di1A485ggnKfp9ykfVvg+NduGm+eqr27be033N6IJ7fuKmF
7Ki1E+sp0IJDMdyikbqoHdbroHu2chwp/1GzFxhx3Vo7kswS/ehSxWpHjhtrGjAF
tSDUFR9q4C+km2A3k1ZNyd9C1w+R9Lr16j9lBQoGWmUgFsRf8ED16FMMSK/1mCC0
1VSydYnYhKmPZxMByTozGTV97wihA0XqXtadbkQoyvcnEuarBvEX3mPA3effqcEG
rreeDp1yzYtQRW85ZASaqgF5CYKhe9NekurZ5Jd+2mQPYRWPFpIFsSHoOLqbfcN1
/S0Si6LZWq0Mi8cbSi/zq+Eh7Q0w+EXP6Goh0A7M4e1dKt+cfGQpeyeg3cF49Cda
RrTkSv2Sl2jbrShqAsG/HNhyBaI/gtLGi9tig9wAoV2zjGw1Ehs0GcwFq2g/f6Zv
W7rdZIBZT1RIQLczuwsEhv6cFrSM5OU2f5fuKnmeI6uvyz0jRqiMncYRjwI6LotB
sWmdTWi6xsctYirJ3Yip5Tqm01asyUuIeiT+eQ6I9CfHLiGBXEzxlK5PrFqfD7gK
LfB1gCN7SAlxKGepmFMwfF+fDrsurL2T2ePxaaNBLjRAzmpSpH0NedMd+eanuHXV
a/iOL2XmVis7iuFyk5M2XtLFfYBff+mxSPA8d9u5kCpoel0tx3iLmw==
-----END CERTIFICATE-----

Some files were not shown because too many files have changed in this diff Show more