Skip to content

Instantly share code, notes, and snippets.

@AVGP
Last active October 2, 2020 14:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AVGP/dce357288ad42ba96796c7ad27d6a184 to your computer and use it in GitHub Desktop.
Save AVGP/dce357288ad42ba96796c7ad27d6a184 to your computer and use it in GitHub Desktop.
UPnP test server (incomplete)
<?xml version="1.0" encoding="utf-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<dlna:X_DLNADOC>DMR-1.50</dlna:X_DLNADOC>
<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>
<friendlyName>YoloCast</friendlyName>
<manufacturer/>
<manufacturerURL/>
<modelDescription>Node.js DLNA server</modelDescription>
<modelName/>
<modelNumber/>
<UDN>uuid:$UUID</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
<SCPDURL>http://$IP:8088/cds.xml</SCPDURL>
<controlURL>http://$IP:8088/cds_ctrl</controlURL>
<eventSubURL>http://$IP:8088/cds_evt</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
<SCPDURL>http://$IP:8088/cms.xml</SCPDURL>
<controlURL>http://$IP:8088/cms_ctrl</controlURL>
<eventSubURL>http://$IP:8088/cms_evt</eventSubURL>
</service>
</serviceList>
<iconList>
<icon>
<mimetype>image/png</mimetype>
<width>48</width>
<height>48</height>
<depth>24</depth>
<url>/icon_large.png</url>
</icon>
<icon>
<mimetype>image/png</mimetype>
<width>24</width>
<height>24</height>
<depth>24</depth>
<url>/icon_small.png</url>
</icon>
</iconList>
</device>
</root>
var datagram = require('dgram')
var http = require('http')
var fs = require('fs')
var path = require('path')
var uuid = require('uuid/v4')
var mime = require('mime')
const MULTICAST_ADDR = '239.255.255.250'
const DEVICE_UUID = 'cafebabe-dead-beef-cafe-c0c0f00dcafe'
const PORT = 1900
const IP = '192.168.1.18'
/*const announcement = 'HTTP/1.1 200 OK\r\n'
+ 'CACHE-CONTROL: max-age=1800\r\n'
+ `DATE: ${Date.now()}\r\n`
+ 'EXT:\r\n'
+ 'Location: http://192.168.1.7:5000/yolo.xml\r\n'
+ 'SERVER: Yolo UPnP\r\n'
+ 'ST: urn:schemas-upnp-org:device:MediaServer:1\r\n'
+ `USN: uuid:${DEVICE_UUID}::urn:schemas-upnp-org:device:MediaServer:1\r\n\r\n`*/
const announcements = [
// 0: general root device announcement
'NOTIFY * HTTP/1.1\r\n'
+ `HOST: 239.255.255.250:${PORT}\r\n`
+ 'CACHE-CONTROL: max-age=1800\r\n'
+ `LOCATION: http://${IP}:8088/dms.xml\r\n`
+ 'NT: upnp:rootdevice\r\n'
+ 'NTS: ssdp:alive\r\n'
+ 'SERVER: unix/5.1 UPnP/1.1 YoloCast/1.0\r\n'
+ `USN: uuid:${DEVICE_UUID}::upnp:rootdevice\r\n`
+ 'BOOTID.UPNP.ORG: 1\r\n'
+ 'CONFIGID.UPNP.ORG: 0\r\n\r\n',
// 1: device announcement
'NOTIFY * HTTP/1.1\r\n'
+ `HOST: 239.255.255.250:${PORT}\r\n`
+ 'CACHE-CONTROL: max-age=1800\r\n'
+ `LOCATION: http://${IP}:8088/dms.xml\r\n`
+ `NT: uuid:${DEVICE_UUID}\r\n`
+ 'NTS: ssdp:alive\r\n'
+ 'SERVER: unix/5.1 UPnP/1.1 YoloCast/1.0\r\n'
+ `USN: uuid:${DEVICE_UUID}\r\n`
+ 'BOOTID.UPNP.ORG: 1\r\n'
+ 'CONFIGID.UPNP.ORG: 0\r\n\r\n',
// 2: device type announcement
'NOTIFY * HTTP/1.1\r\n'
+ 'HOST: 239.255.255.250:${PORT}\r\n'
+ 'CACHE-CONTROL: max-age=1800\r\n'
+ `LOCATION: http://${IP}:8088/dms.xml\r\n`
+ 'NT: urn:schemas-upnp-org:device:MediaServer:1\r\n'
+ 'NTS: ssdp:alive\r\n'
+ 'SERVER: unix/5.1 UPnP/1.1 YoloCast/1.0\r\n'
+ `USN: uuid:${DEVICE_UUID}::urn:schemas-upnp-org:device:MediaServer:1\r\n`
+ 'BOOTID.UPNP.ORG: 1\r\n'
+ 'CONFIGID.UPNP.ORG: 0\r\n\r\n',
// 3: service announcement: Content Directory
'NOTIFY * HTTP/1.1\r\n'
+ `HOST: 239.255.255.250:${PORT}\r\n`
+ 'CACHE-CONTROL: max-age=1800\r\n'
+ `LOCATION: http://${IP}:8088/cds.xml\r\n`
+ 'NT: urn:schemas-upnp-org:service:ContentDirectory:1\r\n'
+ 'NTS: ssdp:alive\r\n'
+ 'SERVER: unix/5.1 UPnP/1.1 YoloCast/1.0\r\n'
+ `USN: uuid:${DEVICE_UUID}::urn:schemas-upnp-org:service:ContentDirectory:1\r\n`
+ 'BOOTID.UPNP.ORG: 1\r\n'
+ 'CONFIGID.UPNP.ORG: 0\r\n\r\n',
// 4: service announcement: Connection Management
'NOTIFY * HTTP/1.1\r\n'
+ `HOST: 239.255.255.250:${PORT}\r\n`
+ 'CACHE-CONTROL: max-age=1800\r\n'
+ `LOCATION: http://${IP}:8088/cms.xml\r\n`
+ 'NT: urn:schemas-upnp-org:service:ConnectionManager:1\r\n'
+ 'NTS: ssdp:alive\r\n'
+ 'SERVER: unix/5.1 UPnP/1.1 YoloCast/1.0\r\n'
+ `USN: uuid:${DEVICE_UUID}::urn:schemas-upnp-org:service:ConnectionManager:1\r\n`
+ 'BOOTID.UPNP.ORG: 1\r\n'
+ 'CONFIGID.UPNP.ORG: 0\r\n\r\n'
]
var ssdpServer = datagram.createSocket('udp4')
var httpServer = http.createServer(function(req, res) {
const fullPath = path.join(__dirname, req.url);
if (req.url.match(/\.xml$/)) return sendHttpControlData(fullPath, res)
console.log('Media request', req.url);
try {
const size = fs.statSync(fullPath).size
res.writeHead(200, {
'Content-Type': mime.getType(fullPath),
'Content-Length': size
});
var stream = fs.createReadStream(fullPath);
stream.pipe(res);
} catch(e) {
res.writeHead(404, 'not found')
}
});
ssdpServer.on('listening', async function() {
console.log('listening...')
// announce root device
ssdpServer.send(Buffer.from(announcements[0]), 1900, '239.255.255.250')
await sleep(200)
ssdpServer.send(Buffer.from(announcements[1]), 1900, '239.255.255.250')
await sleep(200)
ssdpServer.send(Buffer.from(announcements[2]), 1900, '239.255.255.250')
await sleep(200)
ssdpServer.send(Buffer.from(announcements[3]), 1900, '239.255.255.250')
await sleep(200)
ssdpServer.send(Buffer.from(announcements[4]), 1900, '239.255.255.250')
await(1000)
ssdpServer.addMembership(MULTICAST_ADDR)
})
ssdpServer.on('message', (msg, rInfo) => {
if (msg.toString('utf-8').match('M-SEARCH')) {
console.log('RECV', msg.toString('utf-8'), rInfo)
// console.log('announced!', announcement)
// ssdpServer.send(Buffer.from(announcement), 8088, rInfo.address)
}
})
ssdpServer.bind(PORT)
httpServer.listen(8088)
function sendHttpControlData(fullPath, res) {
console.log('Control request', fullPath)
try {
var content = fs.readFileSync(fullPath, 'utf-8');
content = content.replace(/\$UUID/g, DEVICE_UUID)
content = content.replace(/\$IP/g, IP) //TODO: Figure out local IP
} catch(e) {
console.log(fullPath + ' not found')
res.writeHead(404, 'Not found')
res.write('')
return
}
res.writeHead(200, {
'Content-Type': mime.getType(fullPath),
'Content-Length': content.length
});
res.write(content)
return
}
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment