├── .gitignore ├── LICENSE ├── README.md ├── build ├── darwin │ └── .empty ├── freebsd │ └── .empty ├── linux │ └── .empty ├── openbsd │ └── .empty └── windows │ └── .empty ├── config.json.dist ├── crosscompile.bash ├── kd.go └── kd ├── config └── config.go └── service.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/config.json 2 | config.json 3 | kd-go.iml 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 André Wiedemann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kd-go 2 | 3 | [![Join the chat at https://gitter.im/edi-design/kd-go](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/edi-design/kd-go?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | GO-Version of the KabelDeutschland streaming proxy (https://github.com/edi-design/kd-streaming-proxy). 6 | To get more information, visit http://freshest.me. 7 | 8 | ## dependencies 9 | 10 | * gorilla mux router (https://github.com/gorilla/mux) 11 | * `go get github.com/gorilla/mux` 12 | * `go get bitbucket.org/gotamer/cfg` 13 | 14 | ## build 15 | 16 | To build the source for various plattforms, simply run the build-script provided under `scripts/build.sh`. 17 | It relies on a proper configured go-environment with GOPATH and GOROOT set up. 18 | 19 | By now, I only tested the build process on OS X but it should not be a problem to run the commands on Linux. 20 | 21 | ## configuration 22 | 23 | Place a `config.json` next to the binary and fill it with the following content, including your KabelDeutschland credentials. 24 | ``` 25 | { 26 | "Service": { 27 | "Username": "##USERNAME##", 28 | "Password": "##PASSWORD##", 29 | "Listen": ":8787" 30 | } 31 | } 32 | ``` 33 | 34 | ## run 35 | 36 | The easiest way, is to run the binary without any params. It searches automatically for the `config.json` next to the binary. 37 | 38 | `./kd_proxy` 39 | 40 | ### params 41 | 42 | ``` 43 | # ./kd_proxy -h 44 | you need to set the following params: 45 | -c string 46 | specifiy the config.json location, if not next to binary 47 | -h display help message 48 | -no-cache 49 | disables playlist caching 50 | -no-check-certificate 51 | disable root CA check for HTTP requests 52 | -v enable verbose mode to see more debug output. 53 | -version 54 | shows the current version number. 55 | ``` 56 | -------------------------------------------------------------------------------- /build/darwin/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edi-design/kd-go/1dd6e069743011426cdc8358db9ae0947aff6e9b/build/darwin/.empty -------------------------------------------------------------------------------- /build/freebsd/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edi-design/kd-go/1dd6e069743011426cdc8358db9ae0947aff6e9b/build/freebsd/.empty -------------------------------------------------------------------------------- /build/linux/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edi-design/kd-go/1dd6e069743011426cdc8358db9ae0947aff6e9b/build/linux/.empty -------------------------------------------------------------------------------- /build/openbsd/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edi-design/kd-go/1dd6e069743011426cdc8358db9ae0947aff6e9b/build/openbsd/.empty -------------------------------------------------------------------------------- /build/windows/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edi-design/kd-go/1dd6e069743011426cdc8358db9ae0947aff6e9b/build/windows/.empty -------------------------------------------------------------------------------- /config.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "Service": { 3 | "Username": "##USERNAME##", 4 | "Password": "##PASSWORD##", 5 | "Listen": ":8787" 6 | } 7 | } -------------------------------------------------------------------------------- /crosscompile.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## 3 | # kd-go build-script 4 | ## 5 | PACKAGE="kd-proxy" 6 | 7 | type setopt >/dev/null 2>&1 && setopt shwordsplit 8 | PLATFORMS="darwin/386 darwin/amd64 freebsd/386 freebsd/amd64 freebsd/arm/5 freebsd/arm/6 freebsd/arm/7 linux/386 linux/amd64 linux/arm/5 linux/arm/6 linux/arm/7 linux/arm/arm64 linux/ppc64 linux/ppc64le windows/386 windows/amd64 openbsd/386 openbsd/amd64" 9 | 10 | FAILURES="" 11 | for PLATFORM in $PLATFORMS; do 12 | GOOS=${PLATFORM%%/*} 13 | GOARCHWITHTYPE=${PLATFORM#*/} 14 | GOARCH=${GOARCHWITHTYPE%/*} 15 | GOARCHTYPE=${PLATFORM##*/} 16 | OUTPUT="build/$GOOS/${PACKAGE}-${GOARCH}" 17 | 18 | TYPE="" 19 | if [[ $GOARCHTYPE != $GOARCH ]] ; then 20 | TYPE="GO$(echo $GOARCH | tr '[a-z]' '[A-Z]')=${GOARCHTYPE}" 21 | OUTPUT="$OUTPUT-$GOARCHTYPE" 22 | fi 23 | 24 | # prepare cross-compile env for platform 25 | PREPARECMD=" 26 | pushd ${GOROOT}/src/ > /dev/null ; 27 | GOPATH=${GOPATH}:${PWD}/../../../../ GOOS=${GOOS} GOARCH=${GOARCH} ${TYPE} ./make.bash --no-clean 2> /dev/null 1> /dev/null ; 28 | popd > /dev/null" 29 | 30 | # build for platform 31 | BUILDCMD="GOPATH=${GOPATH}:${PWD}/../../../../ GOOS=${GOOS} GOARCH=${GOARCH} ${TYPE} go build -o ${OUTPUT} *.go" 32 | 33 | echo "- preparing $PLATFORM" 34 | eval $PREPARECMD || RPEPFAILURES="$RPEPFAILURES $PLATFORM" 35 | echo "-- done" 36 | 37 | echo "- building $PLATFORM" 38 | eval $BUILDCMD || FAILURES="$FAILURES $PLATFORM" 39 | echo "-- done: $OUTPUT" 40 | done 41 | 42 | if [ "$RPEPFAILURES" != "" ]; then 43 | echo "*** prepare FAILED on $RPEPFAILURES ***" 44 | fi 45 | 46 | if [ "$FAILURES" != "" ]; then 47 | echo "*** build FAILED on $FAILURES ***" 48 | fi -------------------------------------------------------------------------------- /kd.go: -------------------------------------------------------------------------------- 1 | // KabelDeutschland streaming proxy 2 | // Author: andre@freshest.me 3 | // Date: 23.04.2015 4 | // Version: 1 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/edi-design/kd-go/kd" 14 | "github.com/edi-design/kd-go/kd/config" 15 | 16 | "bitbucket.org/gotamer/cfg" 17 | ) 18 | 19 | var ( 20 | version = flag.Bool("version", false, "shows the current version number.") 21 | configFileParam = flag.String("c", "", "specifiy the config.json location, if not next to binary") 22 | Config = &config.Config{} 23 | ) 24 | 25 | const ( 26 | Version = "0.1.3" 27 | ) 28 | 29 | func main() { 30 | flag.Parse() 31 | 32 | if *version { 33 | fmt.Println("KabelDeutschland streaming proxy, http://freshest.me") 34 | fmt.Println(Version) 35 | return 36 | } 37 | 38 | // load config 39 | var cfgFile string 40 | if *configFileParam != "" { 41 | cfgFile = *configFileParam 42 | } else { 43 | dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) 44 | cfgFile = dir + "/config.json" 45 | } 46 | err := cfg.Load(cfgFile, Config) 47 | if err != nil { 48 | cfg.Save(cfgFile, Config) 49 | fmt.Println("\n\tPlease edit your configuration at: ", cfgFile, "\n") 50 | return 51 | } 52 | 53 | // run service 54 | kd.Service(Config) 55 | } 56 | -------------------------------------------------------------------------------- /kd/config/config.go: -------------------------------------------------------------------------------- 1 | // Author: andre@freshest.me 2 | // Date: 23.04.2015 3 | // Version: 1 4 | 5 | // configuration structs and global const. 6 | package config 7 | 8 | // Api configuration struct 9 | type Config struct { 10 | Service struct { 11 | Username string 12 | Password string 13 | Listen string 14 | } 15 | } 16 | 17 | // struct to login to api 18 | type SignIn struct { 19 | DomainID int 20 | SiteGuid string 21 | LoginStatus int 22 | UserData interface{} 23 | } 24 | 25 | type ChannelList []Channel 26 | 27 | // channellist 28 | // ToDo http://freshest.me/how-to-reverse-engineer-the-kabeldeutschland-tv-streaming-api/ 29 | type Channel struct { 30 | MediaID interface{} 31 | MediaName string 32 | MediaTypeID interface{} 33 | MediaTypeName interface{} 34 | Rating interface{} 35 | ViewCounter interface{} 36 | Description interface{} 37 | CreationDate interface{} 38 | LastWatchDate interface{} 39 | StartDate interface{} 40 | CatalogStartDate interface{} 41 | PicURL interface{} 42 | URL interface{} 43 | MediaWebLink interface{} 44 | Duration interface{} 45 | FileID interface{} 46 | MediaDynamicData interface{} 47 | SubDuration interface{} 48 | SubFileFormat interface{} 49 | SubFileID interface{} 50 | SubURL interface{} 51 | GeoBlock interface{} 52 | TotalItems interface{} 53 | like_counter interface{} 54 | Tags interface{} 55 | AdvertisingParameters interface{} 56 | Files []struct { 57 | FileID string 58 | URL string 59 | Duration string 60 | Format string 61 | PreProvider string 62 | PostProvider string 63 | BreakProvider string 64 | OverlayProvider string 65 | BreakPoints string 66 | OverlayPoints string 67 | Language string 68 | IsDefaultLang bool 69 | CoGuid string 70 | } 71 | Pictures interface{} 72 | ExternalIDs interface{} 73 | } 74 | 75 | // response of a licensed link 76 | type LicensedLink struct { 77 | MainUrl string 78 | AltUrl string 79 | } 80 | 81 | // global const to be used in code 82 | const ( 83 | GATEWAY = "https://api-live.iptv.kabel-deutschland.de/v2_9/gateways/jsonpostgw.aspx" 84 | IOS_VERSION = "8.1.2" 85 | APP_VERSION = "1.2.3" 86 | METHOD_SIGNIN = "SSOSignIn" 87 | METHOD_CHANNELLIST = "GetChannelMediaList" 88 | METHOD_LICENSED_LINK = "GetLicensedLinks" 89 | INIT_OBJECT = "eyJBcGlVc2VyIjoidHZwYXBpXzE4MSIsIlVESUQiOiJEMkFDNjMzQUZCNjQ0Q0YwQTY3NTA1MzcwNTc4Q0RFNSIsIkRvbWFpbklEIjozMTUzODQsIlNpdGVHdWlkIjo2Nzk4NzAsIlBsYXRmb3JtIjoiaVBhZCIsIkFwaVBhc3MiOiJhek5ETHpzbktER3RBclZXMlNIUiIsIkxvY2FsZSI6eyJMb2NhbGVEZXZpY2UiOiJudWxsIiwiTG9jYWxlVXNlclN0YXRlIjoiVW5rbm93biIsIkxvY2FsZUNvdW50cnkiOiJudWxsIiwiTG9jYWxlTGFuZ3VhZ2UiOiJudWxsIn19" 90 | CHANNEL_OBJECT = "\"orderBy\":\"None\",\"pageSize\":1000,\"picSize\":\"100X100\",\"ChannelID\":340758" 91 | M3U_HEAD = "#EXTM3U\n" 92 | M3U_LINE = "#EXTINF:-1,%s\n%s\n" 93 | 94 | QUALITY_LOW = "CCURstream564000.m3u8" 95 | QUALITY_MEDIUM = "CCURstream1064000.m3u8" 96 | QUALITY_HIGH = "CCURstream1664000.m3u8" 97 | 98 | CACHE_FILE = "playlist_%s.m3u" 99 | CACHE_LIFETIME = 86400 100 | ) 101 | -------------------------------------------------------------------------------- /kd/service.go: -------------------------------------------------------------------------------- 1 | package kd 2 | 3 | import ( 4 | "crypto/tls" 5 | b64 "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "flag" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net" 13 | "net/http" 14 | "os" 15 | "strings" 16 | "time" 17 | 18 | "github.com/edi-design/kd-go/kd/config" 19 | "github.com/gorilla/mux" 20 | ) 21 | 22 | // struct of json configuration 23 | type MainConfig struct { 24 | Service struct { 25 | Username string 26 | Password string 27 | } 28 | } 29 | 30 | var ( 31 | Config *config.Config 32 | verbose = flag.Bool("v", false, "enable verbose mode to see more debug output.") 33 | noCheckCertParam = flag.Bool("no-check-certificate", false, "disable root CA check for HTTP requests") 34 | noCache = flag.Bool("no-cache", false, "disables playlist caching") 35 | ) 36 | 37 | // main 38 | func Service(ObjConfig *config.Config) { 39 | // write config to environment vars 40 | Config = ObjConfig 41 | 42 | // check credentials 43 | signIn() 44 | 45 | // init router 46 | serv := mux.NewRouter() 47 | 48 | subroute := serv.PathPrefix("/").Subrouter() 49 | subroute.HandleFunc("/", channelHandler).Methods("GET") 50 | subroute.HandleFunc("/{quality}", channelHandler).Methods("GET") 51 | subroute.HandleFunc("/{quality}/{format}", channelHandler).Methods("GET") 52 | 53 | // not found handler. fallback if given path is not set up. 54 | subroute.HandleFunc("/{path:.*}", notFoundHandler) 55 | 56 | // start http-handle 57 | http.Handle("/", serv) 58 | 59 | fmt.Println("== Listening ...") 60 | printInterfaces() 61 | http.ListenAndServe(Config.Service.Listen, nil) 62 | } 63 | 64 | // Default route-handler if no configured endpoint matches. 65 | func notFoundHandler(w http.ResponseWriter, r *http.Request) { 66 | params := mux.Vars(r) 67 | path := params["path"] 68 | 69 | err := errors.New("use known subroutes") 70 | fmt.Println(err) 71 | fmt.Printf("path requested: %s:", path) 72 | 73 | w.WriteHeader(http.StatusNotFound) 74 | } 75 | 76 | // Handles the root directory requests. 77 | func channelHandler(w http.ResponseWriter, r *http.Request) { 78 | // init vars 79 | var result config.ChannelList 80 | var data string 81 | 82 | // debug output 83 | fmt.Println("== Get channellist") 84 | 85 | // get params 86 | params := mux.Vars(r) 87 | format := params["format"] 88 | quality := params["quality"] 89 | 90 | cache_file, quality_playlist := getQualityInformations(quality) 91 | 92 | request_url := getUrl(config.METHOD_CHANNELLIST) 93 | body := "{\"initObj\":" + getInitObj() + "," + config.CHANNEL_OBJECT + "}" 94 | err := httpRequest("POST", request_url, body, &result) 95 | 96 | if err != nil { 97 | fmt.Printf("could not fetch: %v", err) 98 | } 99 | 100 | // read cache 101 | cache_stat, err_cache := os.Stat(cache_file) 102 | if err_cache == nil && (time.Now().Unix()-cache_stat.ModTime().Unix() <= config.CACHE_LIFETIME) { 103 | cached_data, _ := ioutil.ReadFile(cache_file) 104 | data = string(cached_data[:]) 105 | } else { 106 | // call backend 107 | data = data + config.M3U_HEAD 108 | for _, channel := range result { 109 | link, err_link := getLicensedLink(channel.Files[0].FileID, channel.Files[0].URL, quality_playlist) 110 | if err_link != nil { 111 | fmt.Println(err_link.Error()) 112 | data = "This works only if you are using a KabelDeutschland Internet connection.\n" + err_link.Error() 113 | break 114 | } 115 | data = data + fmt.Sprintf(config.M3U_LINE, channel.MediaName, link) 116 | } 117 | 118 | // write cache file 119 | if !*noCache { 120 | ioutil.WriteFile(cache_file, []byte(data), 0644) 121 | } 122 | } 123 | 124 | // set header 125 | if format == "txt" { 126 | w.Header().Set("Content-Type", "text/plain") 127 | } else { 128 | w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") 129 | } 130 | 131 | w.Header().Set("Status", "200 OK") 132 | w.Header().Set("Content-Disposition", "inline; filename=\"playlist.m3u\"") 133 | w.Header().Set("Cache-Control", "no-cache, must-revalidate") 134 | w.Header().Set("Expies", "Sat, 26 Jul 1997 05:00:00 GMT") 135 | 136 | w.Write([]byte(data)) 137 | } 138 | 139 | // get playlist according to requested quality 140 | func getQualityInformations(quality string) (string, string) { 141 | var ( 142 | quality_file string 143 | quality_playlist string 144 | ) 145 | 146 | switch quality { 147 | case "low": 148 | quality_file = fmt.Sprintf(config.CACHE_FILE, quality) 149 | quality_playlist = config.QUALITY_LOW 150 | case "high": 151 | quality_file = fmt.Sprintf(config.CACHE_FILE, quality) 152 | quality_playlist = config.QUALITY_HIGH 153 | default: 154 | quality_file = fmt.Sprintf(config.CACHE_FILE, "medium") 155 | quality_playlist = config.QUALITY_MEDIUM 156 | } 157 | 158 | return quality_file, quality_playlist 159 | } 160 | 161 | // request a link with a valid session 162 | func getLicensedLink(id string, link string, playlist string) (string, error) { 163 | var result config.LicensedLink 164 | 165 | request_url := getUrl(config.METHOD_LICENSED_LINK) 166 | body := "{\"initObj\":" + getInitObj() + ",\"mediaFileId\":" + id + ",\"baseLink\":\"" + string(link[:]) + "\"}" 167 | err := httpRequest("POST", request_url, body, &result) 168 | 169 | if err != nil { 170 | fmt.Printf("could not fetch: %v", err) 171 | return "", errors.New("no link") 172 | } 173 | 174 | resp, err_get := http.Get(result.MainUrl) 175 | 176 | if err_get != nil { 177 | return "", err_get 178 | } 179 | 180 | url := resp.Request.URL.String() 181 | i := strings.LastIndex(url, "/") 182 | 183 | url = url[:i] + "/" + playlist 184 | 185 | return url, nil 186 | } 187 | 188 | // concats params to return a valid API url 189 | func getUrl(method string) string { 190 | return fmt.Sprintf("%s?m=%s&iOSv=%s&Appv=%s", config.GATEWAY, method, config.IOS_VERSION, config.APP_VERSION) 191 | } 192 | 193 | // check credentials 194 | func signIn() { 195 | fmt.Println("== Checking credentials") 196 | 197 | var result config.SignIn 198 | 199 | request_url := getUrl(config.METHOD_SIGNIN) 200 | 201 | // TODO use json.Marshal 202 | body := 203 | "{\"initObj\":" + 204 | getInitObj() + 205 | ",\"userName\":\"" + Config.Service.Username + "\"" + 206 | ",\"password\":\"" + Config.Service.Password + "\"" + 207 | ",\"providerID\":0" + 208 | "}" 209 | 210 | handleError(body) 211 | err := httpRequest("POST", request_url, body, &result) 212 | 213 | switch { 214 | case err != nil, result.LoginStatus != 0: 215 | handleError(fmt.Sprintf("Returned result: %v", result)) 216 | fmt.Println("Credentials are wrong") 217 | os.Exit(1) 218 | } 219 | 220 | fmt.Println("done") 221 | } 222 | 223 | // print interfaces to know where the proxy is listening 224 | func printInterfaces() { 225 | addrs, err := net.InterfaceAddrs() 226 | 227 | if err != nil { 228 | fmt.Println("Can't get interfaces. You have to have at least one network connection.") 229 | log.Fatal("No interface found") 230 | } 231 | 232 | for _, addr := range addrs { 233 | 234 | var ip net.IP 235 | switch v := addr.(type) { 236 | case *net.IPAddr: 237 | case *net.IPNet: 238 | ip = v.IP 239 | } 240 | 241 | if ip == nil || ip.IsLoopback() { 242 | continue 243 | } 244 | 245 | ip = ip.To4() 246 | if ip == nil { 247 | continue // not an ipv4 address 248 | } 249 | fmt.Println("http://" + ip.String() + Config.Service.Listen) 250 | } 251 | } 252 | 253 | // main helper to call any http request. 254 | func httpRequest(method string, url string, body string, result interface{}) error { 255 | var ( 256 | req *http.Request 257 | err error 258 | ) 259 | 260 | // init client, skip cert check, because of some problems with env without root-ca 261 | tr := &http.Transport{} 262 | if *noCheckCertParam { 263 | tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 264 | handleError("= certificate check disabled") 265 | } 266 | client := &http.Client{Transport: tr} 267 | 268 | switch method { 269 | case "GET": 270 | req, err = http.NewRequest(method, url, nil) 271 | case "POST": 272 | req, err = http.NewRequest(method, url, strings.NewReader(body)) 273 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") 274 | default: 275 | return errors.New(method + " is not a valid method.") 276 | } 277 | 278 | if err != nil { 279 | handleError(fmt.Sprintf("could not stat request: %v", err)) 280 | return err 281 | } 282 | 283 | resp, err := client.Do(req) 284 | if err != nil { 285 | handleError(fmt.Sprintf("could not fetch: %v", err)) 286 | return err 287 | } 288 | 289 | decoder := json.NewDecoder(resp.Body) 290 | err = decoder.Decode(&result) 291 | if err != nil { 292 | handleError(fmt.Sprintf("could not decode response: %v", err)) 293 | } 294 | 295 | return err 296 | } 297 | 298 | // handle verbose mode otuput 299 | func handleError(message string) { 300 | if *verbose { 301 | log.Print(message) 302 | } 303 | } 304 | 305 | // init obj 306 | func getInitObj() string { 307 | initObject, _ := b64.StdEncoding.DecodeString(config.INIT_OBJECT) 308 | return string(initObject) 309 | } 310 | --------------------------------------------------------------------------------