<commit_before>package server

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"text/template"

	"github.com/aykevl93/plaincast/apps"
	"github.com/aykevl93/plaincast/apps/youtube"
)

// This implements a UPnP/DIAL server.
// DIAL is deprecated, but it's still being used by the YouTube app on Android.

// UPnP device description template
const DEVICE_DESCRIPTION = `<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="{{.ConfigId}}">
	<specVersion>
		<major>1</major>
		<minor>1</minor>
	</specVersion>
	<device>
		<deviceType>urn:schemas-upnp-org:device:dial:1</deviceType>
		<friendlyName>{{.FriendlyName}}</friendlyName>
		<manufacturer>-</manufacturer>
		<modelDescription>Play the audio of YouTube videos</modelDescription>
		<modelName>{{.ModelName}}</modelName>
		<modelNumber>{{.ModelNumber}}</modelNumber>
		<UDN>uuid:{{.DeviceUUID}}</UDN>
		<serviceList>
			<service>
				<serviceType>urn:schemas-upnp-org:service:dail:1</serviceType>
				<serviceId>urn:upnp-org:serviceId:dail</serviceId>
				<SCPDURL>/upnp/notfound</SCPDURL>
				<controlURL>/upnp/notfound</controlURL>
				<eventSubURL></eventSubURL>
			</service>
		</serviceList>
	</device>
</root>
`

// DIAL app template
const APP_RESPONSE = `<?xml version="1.0" encoding="UTF-8"?>
<service xmlns="urn:dial-multiscreen-org:schemas:dial" dialVer="1.7">
	<name>{{.name}}</name> 
	<options allowStop="true"/> 
	<state>{{.state}}</state> 
{{if .runningUrl}}
	<link rel="run" href="{{.runningUrl}}"/>
{{end}}
</service>
`

type UPnPServer struct {
	descriptionTemplate *template.Template
	appStateTemplate    *template.Template
	httpPort            int
	apps                map[string]apps.App
	friendlyName        string
	appMatchString      *regexp.Regexp
	proxyClient         *http.Client
}

func NewUPnPServer() *UPnPServer {
	us := &UPnPServer{}

	us.appMatchString = regexp.MustCompile("^/apps/([a-zA-Z]+)(/run)?$")
	hostname, err := os.Hostname()
	if err != nil {
		panic(err)
	}
	us.friendlyName = FRIENDLY_NAME + " " + hostname

	// initialize all known apps
	us.apps = make(map[string]apps.App)
	us.apps["YouTube"] = youtube.New(FRIENDLY_NAME)

	// http Client as used by the proxy
	us.proxyClient = &http.Client{}

	http.HandleFunc("/upnp/description.xml", us.serveDescription)
	http.HandleFunc("/apps/", us.serveApp)
	http.HandleFunc("/proxy/", us.serveProxy)
	http.HandleFunc("/hacks/silence.wav", us.serveSilence)

	return us
}

func (us *UPnPServer) startServing() (int, error) {
	if us.httpPort != 0 {
		return 0, errors.New("already serving")
	}

	httpPort, err := serveZeroHTTPPort(nil)
	if err != nil {
		return 0, err
	}

	us.httpPort = httpPort

	return us.httpPort, nil
}

// serveDescription serves the UPnP device description
func (us *UPnPServer) serveDescription(w http.ResponseWriter, req *http.Request) {
	log.Println("http", req.Method, req.URL.Path)

	w.Header().Set("Application-URL", fmt.Sprintf("http://%s:%d/apps/", getUrlIP(getLocalAddr(req)), us.httpPort))

	deviceDescription := map[string]interface{}{
		"ConfigId":     CONFIGID,
		"FriendlyName": us.friendlyName,
		"ModelName":    NAME,
		"ModelNumber":  VERSION,
		"DeviceUUID":   deviceUUID,
	}

	if us.descriptionTemplate == nil {
		tmpl, err := template.New("").Parse(DEVICE_DESCRIPTION)
		if err != nil {
			panic(err)
		}
		us.descriptionTemplate = tmpl
	}

	w.Header().Set("Content-Type", "text/xml; charset=utf-8")
	err := us.descriptionTemplate.Execute(w, deviceDescription)
	if err != nil {
		panic(err)
	}
}

// serveApp serves an app description and handles starting/stopping of apps
func (us *UPnPServer) serveApp(w http.ResponseWriter, req *http.Request) {
	log.Println("http", req.Method, req.URL.Path)

	matches := us.appMatchString.FindSubmatch([]byte(req.URL.Path))
	if matches == nil || len(matches) < 3 {
		w.WriteHeader(404)
		return
	}

	appName := string(matches[1])

	app, ok := us.apps[appName]
	if !ok {
		w.WriteHeader(404)
		return
	}

	if len(matches[2]) > 0 {
		if req.Method != "DELETE" {
			panic("expected delete on '" + req.URL.Path + "', not " + req.Method)
		}
		app.Quit()
		return
	}

	switch req.Method {
	case "GET":
	case "POST":
		length, err := strconv.Atoi(req.Header["Content-Length"][0])
		if err != nil {
			panic(err)
		}

		buf := make([]byte, length)
		_, err = io.ReadFull(req.Body, buf)
		if err != nil {
			panic(err)
		}
		message := string(buf)

		if !app.Running() {
			app.Start(message)

			w.WriteHeader(201)
			laddr := getLocalAddr(req)
			runningAppUrl := fmt.Sprintf("http://%s:%d/apps/%s/run", getUrlIP(laddr), us.httpPort, appName)
			w.Header().Set("Location", runningAppUrl)
			w.Header().Set("Content-Length", "0")
			return
		}

	default:
		panic("unimplemented method: " + req.Method)
	}

	status := "stopped"
	runningUrl := ""
	if app.Running() {
		status = "running"
		runningUrl = "run"
	}

	if us.appStateTemplate == nil {
		tmpl, err := template.New("").Parse(APP_RESPONSE)
		if err != nil {
			panic(err)
		}
		us.appStateTemplate = tmpl
	}

	appResponse := map[string]interface{}{
		"name":       appName,
		"state":      status,
		"runningUrl": runningUrl,
	}

	w.Header().Set("Content-Type", "text/xml; charset=utf-8")
	err := us.appStateTemplate.Execute(w, appResponse)
	if err != nil {<commit_msg>Allow more than one connected device via DIAL<commit_after>package server

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"text/template"

	"github.com/aykevl93/plaincast/apps"
	"github.com/aykevl93/plaincast/apps/youtube"
)

// This implements a UPnP/DIAL server.
// DIAL is deprecated, but it's still being used by the YouTube app on Android.

// UPnP device description template
const DEVICE_DESCRIPTION = `<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="{{.ConfigId}}">
	<specVersion>
		<major>1</major>
		<minor>1</minor>
	</specVersion>
	<device>
		<deviceType>urn:schemas-upnp-org:device:dial:1</deviceType>
		<friendlyName>{{.FriendlyName}}</friendlyName>
		<manufacturer>-</manufacturer>
		<modelDescription>Play the audio of YouTube videos</modelDescription>
		<modelName>{{.ModelName}}</modelName>
		<modelNumber>{{.ModelNumber}}</modelNumber>
		<UDN>uuid:{{.DeviceUUID}}</UDN>
		<serviceList>
			<service>
				<serviceType>urn:schemas-upnp-org:service:dail:1</serviceType>
				<serviceId>urn:upnp-org:serviceId:dail</serviceId>
				<SCPDURL>/upnp/notfound</SCPDURL>
				<controlURL>/upnp/notfound</controlURL>
				<eventSubURL></eventSubURL>
			</service>
		</serviceList>
	</device>
</root>
`

// DIAL app template
const APP_RESPONSE = `<?xml version="1.0" encoding="UTF-8"?>
<service xmlns="urn:dial-multiscreen-org:schemas:dial" dialVer="1.7">
	<name>{{.name}}</name> 
	<options allowStop="false"/> 
	<state>{{.state}}</state> 
{{if .runningUrl}}
	<link rel="run" href="{{.runningUrl}}"/>
{{end}}
</service>
`

type UPnPServer struct {
	descriptionTemplate *template.Template
	appStateTemplate    *template.Template
	httpPort            int
	apps                map[string]apps.App
	friendlyName        string
	appMatchString      *regexp.Regexp
	proxyClient         *http.Client
}

func NewUPnPServer() *UPnPServer {
	us := &UPnPServer{}

	us.appMatchString = regexp.MustCompile("^/apps/([a-zA-Z]+)(/run)?$")
	hostname, err := os.Hostname()
	if err != nil {
		panic(err)
	}
	us.friendlyName = FRIENDLY_NAME + " " + hostname

	// initialize all known apps
	us.apps = make(map[string]apps.App)
	us.apps["YouTube"] = youtube.New(FRIENDLY_NAME)

	// http Client as used by the proxy
	us.proxyClient = &http.Client{}

	http.HandleFunc("/upnp/description.xml", us.serveDescription)
	http.HandleFunc("/apps/", us.serveApp)
	http.HandleFunc("/proxy/", us.serveProxy)
	http.HandleFunc("/hacks/silence.wav", us.serveSilence)

	return us
}

func (us *UPnPServer) startServing() (int, error) {
	if us.httpPort != 0 {
		return 0, errors.New("already serving")
	}

	httpPort, err := serveZeroHTTPPort(nil)
	if err != nil {
		return 0, err
	}

	us.httpPort = httpPort

	return us.httpPort, nil
}

// serveDescription serves the UPnP device description
func (us *UPnPServer) serveDescription(w http.ResponseWriter, req *http.Request) {
	log.Println("http", req.Method, req.URL.Path)

	w.Header().Set("Application-URL", fmt.Sprintf("http://%s:%d/apps/", getUrlIP(getLocalAddr(req)), us.httpPort))

	deviceDescription := map[string]interface{}{
		"ConfigId":     CONFIGID,
		"FriendlyName": us.friendlyName,
		"ModelName":    NAME,
		"ModelNumber":  VERSION,
		"DeviceUUID":   deviceUUID,
	}

	if us.descriptionTemplate == nil {
		tmpl, err := template.New("").Parse(DEVICE_DESCRIPTION)
		if err != nil {
			panic(err)
		}
		us.descriptionTemplate = tmpl
	}

	w.Header().Set("Content-Type", "text/xml; charset=utf-8")
	err := us.descriptionTemplate.Execute(w, deviceDescription)
	if err != nil {
		panic(err)
	}
}

// serveApp serves an app description and handles starting/stopping of apps
func (us *UPnPServer) serveApp(w http.ResponseWriter, req *http.Request) {
	log.Println("http", req.Method, req.URL.Path)

	matches := us.appMatchString.FindSubmatch([]byte(req.URL.Path))
	if matches == nil || len(matches) < 3 {
		w.WriteHeader(404)
		return
	}

	appName := string(matches[1])

	app, ok := us.apps[appName]
	if !ok {
		w.WriteHeader(404)
		return
	}

	if len(matches[2]) > 0 {
		if req.Method != "DELETE" {
			panic("expected delete on '" + req.URL.Path + "', not " + req.Method)
		}
		// This is a hidden feature. It is not advertized, but still supported,
		// to make it easy to re-enable the DELETE method.
		app.Quit()
		return
	}

	switch req.Method {
	case "GET":
	case "POST":
		length, err := strconv.Atoi(req.Header["Content-Length"][0])
		if err != nil {
			panic(err)
		}

		buf := make([]byte, length)
		_, err = io.ReadFull(req.Body, buf)
		if err != nil {
			panic(err)
		}
		message := string(buf)

		app.Start(message)

		w.WriteHeader(201)
		laddr := getLocalAddr(req)
		runningAppUrl := fmt.Sprintf("http://%s:%d/apps/%s/run", getUrlIP(laddr), us.httpPort, appName)
		w.Header().Set("Location", runningAppUrl)
		w.Header().Set("Content-Length", "0")
		return

	default:
		panic("unimplemented method: " + req.Method)
	}

	status := "stopped"
	runningUrl := ""
	if app.Running() {
		status = "running"
		runningUrl = "run"
	}

	if us.appStateTemplate == nil {
		tmpl, err := template.New("").Parse(APP_RESPONSE)
		if err != nil {
			panic(err)
		}
		us.appStateTemplate = tmpl
	}

	appResponse := map[string]interface{}{
		"name":       appName,
		"state":      status,
		"runningUrl": runningUrl,
	}

	w.Header().Set("Content-Type", "text/xml; charset=utf-8")
	err := us.appStateTemplate.Execute(w, appResponse)
	if err != nil {
{'fraction_non_alphanumeric': 0.16879723803303162, 'fraction_numerical': 0.006904917420920033, 'mean_word_length': 4.0130963517305895, 'pattern_counts': {'":': 20, '<': 101, '<?xml version=': 4, '>': 101, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 14}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4378724', 'n_tokens_mistral': 4215, 'n_tokens_neox': 3883, 'n_words': 770}