Big HTTP header parsing refactoring

This commit is contained in:
Ziver Koc 2016-02-19 20:28:26 +01:00
parent 946953699f
commit 862bc7763e
50 changed files with 3114 additions and 3072 deletions

View file

@ -1,243 +1,249 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.io.StringOutputStream;
import zutil.log.LogUtil;
import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPrintStream;
import zutil.net.threaded.ThreadedUDPNetwork;
import zutil.net.threaded.ThreadedUDPNetworkThread;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* An SSDP client class that will request
* service information.
*
* @author Ziver
*/
public class SSDPClient extends ThreadedUDPNetwork implements ThreadedUDPNetworkThread{
private static final Logger logger = LogUtil.getLogger();
/** Mapping of search targets and list of associated services **/
private HashMap<String, LinkedList<StandardSSDPInfo>> services_st;
/** Map of all unique services received **/
private HashMap<String, StandardSSDPInfo> services_usn;
private SSDPServiceListener listener;
/**
* Creates new instance of this class. An UDP
* listening socket at the SSDP port.
*
* @throws IOException
*/
public SSDPClient() throws IOException{
super(null);
super.setThread(this);
services_st = new HashMap<>();
services_usn = new HashMap<>();
}
/**
* Sends an request for an service
*
* @param searchTarget is the SearchTarget of the service
*
* ***** REQUEST:
* M-SEARCH * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* Man: "ssdp:discover"
* ST: ge:fridge
* MX: 3
*
*/
public void requestService(String searchTarget){
requestService(searchTarget, null);
}
public void requestService(String searchTarget, HashMap<String,String> headers){
try {
// Generate an SSDP discover message
StringOutputStream msg = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HttpMessageType.REQUEST );
http.setRequestType("M-SEARCH");
http.setRequestURL("*");
http.setHeader("Host", SSDPServer.SSDP_MULTICAST_ADDR +":"+ SSDPServer.SSDP_PORT );
http.setHeader("ST", searchTarget );
http.setHeader("Man", "\"ssdp:discover\"" );
http.setHeader("MX", "3" );
if(headers != null) {
for (String key : headers.keySet()) {
http.setHeader(key, headers.get(key));
}
}
logger.log(Level.FINEST, "Sending Multicast: "+ http);
http.flush();
byte[] data = msg.toString().getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length,
InetAddress.getByName( SSDPServer.SSDP_MULTICAST_ADDR ),
SSDPServer.SSDP_PORT );
super.send( packet );
http.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Set a listener that will be notified when new services are detected
*/
public void setListener(SSDPServiceListener listener){
this.listener = listener;
}
/**
* Returns a list of received services by
* the given search target.
*
* @param searchTarget is the search target
* @return a list of received services
*/
public LinkedList<StandardSSDPInfo> getServices(String searchTarget){
if(services_st.get(searchTarget) == null)
return new LinkedList<>();
return services_st.get(searchTarget);
}
/**
* Returns the amount of services in the search target
*
* @param searchTarget is the search target
* @return the amount of services cached
*/
public int getServicesCount(String searchTarget){
if(services_st.containsKey(searchTarget)){
return services_st.get(searchTarget).size();
}
return 0;
}
/**
* Returns a service with the given USN.
*
* @param usn is the unique identifier for a service
* @return an service, null if there is no such service
*/
public StandardSSDPInfo getService(String usn){
return services_usn.get( usn );
}
/**
* Clears all the received information of the services
*/
public void clearServices(){
services_usn.clear();
services_st.clear();
}
/**
* Clears all services matching the search target
*/
public void clearServices(String st){
if(services_st.get(st) != null) {
for (StandardSSDPInfo service : services_st.get(st)) {
services_usn.remove(service.getUSN());
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.io.StringOutputStream;
import zutil.log.LogUtil;
import zutil.net.http.HttpHeader;
import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPrintStream;
import zutil.net.threaded.ThreadedUDPNetwork;
import zutil.net.threaded.ThreadedUDPNetworkThread;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* An SSDP client class that will request
* service information.
*
* @author Ziver
*/
public class SSDPClient extends ThreadedUDPNetwork implements ThreadedUDPNetworkThread{
private static final Logger logger = LogUtil.getLogger();
/** Mapping of search targets and list of associated services **/
private HashMap<String, LinkedList<StandardSSDPInfo>> services_st;
/** Map of all unique services received **/
private HashMap<String, StandardSSDPInfo> services_usn;
private SSDPServiceListener listener;
/**
* Creates new instance of this class. An UDP
* listening socket at the SSDP port.
*
* @throws IOException
*/
public SSDPClient() throws IOException{
super(null);
super.setThread(this);
services_st = new HashMap<>();
services_usn = new HashMap<>();
}
/**
* Sends an request for an service
*
* @param searchTarget is the SearchTarget of the service
*
* ***** REQUEST:
* M-SEARCH * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* Man: "ssdp:discover"
* ST: ge:fridge
* MX: 3
*
*/
public void requestService(String searchTarget){
requestService(searchTarget, null);
}
public void requestService(String searchTarget, HashMap<String,String> headers){
try {
// Generate an SSDP discover message
StringOutputStream msg = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HttpMessageType.REQUEST );
http.setRequestType("M-SEARCH");
http.setRequestURL("*");
http.setHeader("Host", SSDPServer.SSDP_MULTICAST_ADDR +":"+ SSDPServer.SSDP_PORT );
http.setHeader("ST", searchTarget );
http.setHeader("Man", "\"ssdp:discover\"" );
http.setHeader("MX", "3" );
if(headers != null) {
for (String key : headers.keySet()) {
http.setHeader(key, headers.get(key));
}
}
logger.log(Level.FINEST, "Sending Multicast: "+ http);
http.flush();
byte[] data = msg.toString().getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length,
InetAddress.getByName( SSDPServer.SSDP_MULTICAST_ADDR ),
SSDPServer.SSDP_PORT );
super.send( packet );
http.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Set a listener that will be notified when new services are detected
*/
public void setListener(SSDPServiceListener listener){
this.listener = listener;
}
/**
* Returns a list of received services by
* the given search target.
*
* @param searchTarget is the search target
* @return a list of received services
*/
public LinkedList<StandardSSDPInfo> getServices(String searchTarget){
if(services_st.get(searchTarget) == null)
return new LinkedList<>();
return services_st.get(searchTarget);
}
/**
* Returns the amount of services in the search target
*
* @param searchTarget is the search target
* @return the amount of services cached
*/
public int getServicesCount(String searchTarget){
if(services_st.containsKey(searchTarget)){
return services_st.get(searchTarget).size();
}
return 0;
}
/**
* Returns a service with the given USN.
*
* @param usn is the unique identifier for a service
* @return an service, null if there is no such service
*/
public StandardSSDPInfo getService(String usn){
return services_usn.get( usn );
}
/**
* Clears all the received information of the services
*/
public void clearServices(){
services_usn.clear();
services_st.clear();
}
/**
* Clears all services matching the search target
*/
public void clearServices(String st){
if(services_st.get(st) != null) {
for (StandardSSDPInfo service : services_st.get(st)) {
services_usn.remove(service.getUSN());
}
}
}
/**
* Waits for responses
*
* ***** RESPONSE;
* HTTP/1.1 200 OK
* Ext:
* Cache-Control: no-cache="Ext", max-age = 5000
* ST: ge:fridge
* USN: uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6
* Location: http://localhost:80
*/
public void receivedPacket(DatagramPacket packet, ThreadedUDPNetwork network) {
try {
String msg = new String(packet.getData(), packet.getOffset(), packet.getLength());
HttpHeaderParser headerParser = new HttpHeaderParser(msg);
HttpHeader header = headerParser.read();
logger.log(Level.FINEST, "Received(from: " + packet.getAddress() + "): " + header);
String usn = header.getHeader("USN");
String st = header.getHeader("ST");
boolean newService = false;
StandardSSDPInfo service;
// Get existing service
if (services_usn.containsKey(usn)) {
service = services_usn.get(usn);
}
// Add new service
else {
newService = true;
service = new StandardSSDPInfo();
services_usn.put(usn, service);
if (!services_st.containsKey(st))
services_st.put(st, new LinkedList<StandardSSDPInfo>());
services_st.get(header.getHeader("ST")).add(service);
}
service.setLocation(header.getHeader("LOCATION"));
service.setST(st);
service.setUSN(usn);
service.setInetAddress(packet.getAddress());
if (header.getHeader("Cache-Control") != null) {
service.setExpirationTime(
System.currentTimeMillis() + 1000 * getCacheTime(header.getHeader("Cache-Control")));
}
service.readHeaders(header);
if (listener != null && newService)
listener.newService(service);
} catch (IOException e){
logger.log(Level.SEVERE, null, e);
}
}
/**
* Waits for responses
*
* ***** RESPONSE;
* HTTP/1.1 200 OK
* Ext:
* Cache-Control: no-cache="Ext", max-age = 5000
* ST: ge:fridge
* USN: uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6
* Location: http://localhost:80
*/
public void receivedPacket(DatagramPacket packet, ThreadedUDPNetwork network) {
String msg = new String(packet.getData(), packet.getOffset(), packet.getLength());
HttpHeaderParser header = new HttpHeaderParser( msg );
logger.log(Level.FINEST, "Received(from: "+packet.getAddress()+"): "+ header);
String usn = header.getHeader("USN");
String st = header.getHeader("ST");
boolean newService = false;
StandardSSDPInfo service;
// Get existing service
if( services_usn.containsKey( usn )){
service = services_usn.get( usn );
}
// Add new service
else{
newService = true;
service = new StandardSSDPInfo();
services_usn.put( usn, service);
if( !services_st.containsKey(st) )
services_st.put( st, new LinkedList<StandardSSDPInfo>() );
services_st.get( header.getHeader("ST") ).add( service );
}
service.setLocation( header.getHeader("LOCATION") );
service.setST( st );
service.setUSN( usn );
service.setInetAddress(packet.getAddress());
if(header.getHeader("Cache-Control") != null) {
service.setExpirationTime(
System.currentTimeMillis() + 1000 * getCacheTime(header.getHeader("Cache-Control")));
}
service.readHeaders(header);
if(listener != null && newService)
listener.newService(service);
}
private long getCacheTime(String cache_control){
long ret = 0;
String[] tmp = cache_control.split(",");
for( String element : tmp ){
element = element.replaceAll("\\s", "").toLowerCase();
if( element.startsWith("max-age=") ){
ret = Long.parseLong( element.substring( "max-age=".length() ) );
}
}
return ret;
}
public interface SSDPServiceListener{
public void newService(StandardSSDPInfo service);
}
}
}
private long getCacheTime(String cache_control){
long ret = 0;
String[] tmp = cache_control.split(",");
for( String element : tmp ){
element = element.replaceAll("\\s", "").toLowerCase();
if( element.startsWith("max-age=") ){
ret = Long.parseLong( element.substring( "max-age=".length() ) );
}
}
return ret;
}
public interface SSDPServiceListener{
public void newService(StandardSSDPInfo service);
}
}

View file

@ -1,38 +1,38 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPrintStream;
/**
* Created by Ziver on 2014-11-07.
*/
public interface SSDPCustomInfo extends SSDPServiceInfo{
public void readHeaders(HttpHeaderParser http);
public void writeHeaders(HttpPrintStream http);
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.net.http.HttpHeader;
import zutil.net.http.HttpPrintStream;
/**
* Created by Ziver on 2014-11-07.
*/
public interface SSDPCustomInfo extends SSDPServiceInfo{
public void readHeaders(HttpHeader http);
public void writeHeaders(HttpPrintStream http);
}

View file

@ -1,334 +1,336 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.StringUtil;
import zutil.io.StringOutputStream;
import zutil.log.LogUtil;
import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPrintStream;
import zutil.net.threaded.ThreadedUDPNetwork;
import zutil.net.threaded.ThreadedUDPNetworkThread;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Server class that announces an service by the SSDP
* protocol specified at:
* http://coherence.beebits.net/chrome/site/draft-cai-ssdp-v1-03.txt
* ftp://ftp.pwg.org/pub/pwg/www/hypermail/ps/att-0188/01-psi_SSDP.pdf
*
* @author Ziver
*
* ********* Message clarification:
* ****** Incoming:
* ST: Search Target, this is object of the discovery request, (e.g., ssdp:all, etc.)
* HOST: This is the SSDP multicast address
* MAN: Description of packet type, (e.g., "ssdp:discover", )
* MX: Wait these few seconds and then send response
*
* ****** Outgoing:
* EXT: required by HTTP - not used with SSDP
* SERVER: informational
* LOCATION: This is the URL to request the QueryEndpointsInterface endpoint
* USN: advertisement UUID
* CACHE-CONTROL: max-age = seconds until advertisement expires
* NT: Notify target same as ST
* NTS: same as Man but for Notify messages
*/
public class SSDPServer extends ThreadedUDPNetwork implements ThreadedUDPNetworkThread{
private static final Logger logger = LogUtil.getLogger();
public static final String SERVER_INFO = "Zutil SSDP Java Server";
public static final int DEFAULT_CACHE_TIME = 60*30; // 30 min
public static final int BUFFER_SIZE = 512;
public static final String SSDP_MULTICAST_ADDR = "239.255.255.250";
public static final int SSDP_PORT = 1900;
// instance specific values
private int cache_time;
private NotifyTimer notifyTimer = null;
/** HashMap that contains services as < SearchTargetName, SSDPServiceInfo > */
private HashMap<String, SSDPServiceInfo> services;
public SSDPServer() throws IOException{
super( null, SSDP_MULTICAST_ADDR, SSDP_PORT );
super.setThread( this );
services = new HashMap<String, SSDPServiceInfo>();
setChacheTime( DEFAULT_CACHE_TIME );
enableNotify( true );
}
/**
* Adds an service that will be announced.
*
* @param service add a new service to be announced
*/
public void addService(SSDPServiceInfo service){
services.put( service.getSearchTarget(), service );
}
/**
* Remove a service from being announced. This function will
* send out an byebye message to the clients that the service is down.
*
* @param searchTarget is the ST value in SSDP
*/
public void removeService(String searchTarget){
sendByeBye( searchTarget );
services.remove( searchTarget );
}
/**
* Sets the cache time that will be sent to
* the clients. If notification is enabled then an
* notification message will be sent every cache_time/2 seconds
*
* @param time is the time in seconds
*/
public void setChacheTime(int time){
cache_time = time;
if( isNotifyEnabled() ){
enableNotify(false);
enableNotify(true);
}
}
/**
* Enable or disable notification messages to clients
* every cache_time/2 seconds
*/
public void enableNotify(boolean enable){
if( enable && notifyTimer==null ){
notifyTimer = new NotifyTimer();
Timer timer = new Timer();
timer.schedule(new NotifyTimer(), 0, cache_time*1000/2);
}else if( !enable && notifyTimer!=null ){
notifyTimer.cancel();
notifyTimer = null;
}
}
/**
* @return if notification messages is enabled
*/
public boolean isNotifyEnabled(){
return notifyTimer != null;
}
/**
* Handles the incoming packets like this:
*
* ***** REQUEST:
* M-SEARCH * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* Man: "ssdp:discover"
* ST: ge:fridge
* MX: 3
*
* ***** RESPONSE;
* HTTP/1.1 200 OK
* Ext:
* Cache-Control: no-cache="Ext", max-age = 5000
* ST: ge:fridge
* USN: uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6
* Location: http://localhost:80
*
*/
public void receivedPacket(DatagramPacket packet, ThreadedUDPNetwork network) {
try {
String msg = new String( packet.getData(), packet.getOffset(), packet.getLength() );
HttpHeaderParser header = new HttpHeaderParser( msg );
// ******* Respond
// Check that the message is an ssdp discovery message
if( header.getRequestType() != null && header.getRequestType().equalsIgnoreCase("M-SEARCH") ){
String man = header.getHeader("Man");
if(man != null)
man = StringUtil.trim(man, '\"');
String st = header.getHeader("ST");
// Check that its the correct URL and that its an ssdp:discover message
if( header.getRequestURL().equals("*") && "ssdp:discover".equalsIgnoreCase(man) ){
// Check if the requested service exists
if( services.containsKey( st ) ){
logger.log(Level.FINEST, "Received Multicast(from: "+packet.getAddress()+"): "+ header);
// Generate the SSDP response
StringOutputStream response = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( response );
http.setStatusCode(200);
http.setHeader("Location", services.get(st).getLocation() );
http.setHeader("USN", services.get(st).getUSN() );
http.setHeader("Server", SERVER_INFO );
http.setHeader("ST", st );
http.setHeader("EXT", "" );
http.setHeader("Cache-Control", "max-age = "+ cache_time );
if(services.get(st) instanceof SSDPCustomInfo)
((SSDPCustomInfo)services.get(st)).writeHeaders(http);
logger.log(Level.FINEST, "Sending Response: "+ http);
http.flush();
String strData = response.toString();
byte[] data = strData.getBytes();
packet = new DatagramPacket(
data, data.length,
packet.getAddress(),
packet.getPort());
network.send( packet );
http.close();
}
}
}
} catch (IOException e) {
logger.log(Level.SEVERE, null, e);
}
}
/**
* This thread is a timer task that sends an
* notification message to the network every
* cache_time/2 seconds.
*
* @author Ziver
*/
private class NotifyTimer extends TimerTask {
public void run(){
sendNotify();
}
}
/**
* Sends keep-alive messages to update the cache of the clients
*/
public void sendNotify(){
for(String st : services.keySet()){
sendNotify( st );
}
}
/**
* Sends an keepalive message to update the cache of the clients
*
* @param searchTarget is the ST value of the service
*
* ********** Message ex:
* NOTIFY * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* NT: blenderassociation:blender
* NTS: ssdp:alive
* USN: someunique:idscheme3
* Location: http://localhost:80
* Cache-Control: max-age = 7393
*/
public void sendNotify(String searchTarget){
try {
SSDPServiceInfo service = services.get(searchTarget);
// Generate the SSDP response
StringOutputStream msg = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HttpMessageType.REQUEST );
http.setRequestType("NOTIFY");
http.setRequestURL("*");
http.setHeader("Server", SERVER_INFO );
http.setHeader("Host", SSDP_MULTICAST_ADDR+":"+SSDP_PORT );
http.setHeader("NT", searchTarget );
http.setHeader("NTS", "ssdp:alive" );
http.setHeader("Location", service.getLocation() );
http.setHeader("Cache-Control", "max-age = "+cache_time );
http.setHeader("USN", service.getUSN() );
if(service instanceof SSDPCustomInfo)
((SSDPCustomInfo) service).writeHeaders(http);
logger.log(Level.FINEST, "Sending Notification: " + http);
http.flush();
byte[] data = msg.toString().getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length,
InetAddress.getByName( SSDP_MULTICAST_ADDR ),
SSDP_PORT );
super.send( packet );
http.close();
} catch (Exception e) {
logger.log(Level.SEVERE, null, e);
}
}
/**
* Shutdown message is sent to the clients that all
* the service is shutting down.
*/
public void sendByeBye(){
for(String st : services.keySet()){
sendByeBye( st );
}
}
/**
* Shutdown message is sent to the clients that the service is shutting down
*
* @param searchTarget is the ST value of the service
*
* ********** Message ex:
* NOTIFY * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* NT: someunique:idscheme3
* NTS: ssdp:byebye
* USN: someunique:idscheme3
*/
public void sendByeBye(String searchTarget){
try {
// Generate the SSDP response
StringOutputStream msg = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HttpMessageType.REQUEST );
http.setRequestType("NOTIFY");
http.setRequestURL("*");
http.setHeader("Server", SERVER_INFO );
http.setHeader("Host", SSDP_MULTICAST_ADDR+":"+SSDP_PORT );
http.setHeader("NT", searchTarget );
http.setHeader("NTS", "ssdp:byebye" );
http.setHeader("USN", services.get(searchTarget).getUSN() );
logger.log(Level.FINEST, "Sending ByeBye: " + http);
http.flush();
byte[] data = msg.toString().getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length,
InetAddress.getByName( SSDP_MULTICAST_ADDR ),
SSDP_PORT );
super.send( packet );
http.close();
} catch (Exception e) {
logger.log(Level.SEVERE, null, e);
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.StringUtil;
import zutil.io.StringOutputStream;
import zutil.log.LogUtil;
import zutil.net.http.HttpHeader;
import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPrintStream;
import zutil.net.threaded.ThreadedUDPNetwork;
import zutil.net.threaded.ThreadedUDPNetworkThread;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Server class that announces an service by the SSDP
* protocol specified at:
* http://coherence.beebits.net/chrome/site/draft-cai-ssdp-v1-03.txt
* ftp://ftp.pwg.org/pub/pwg/www/hypermail/ps/att-0188/01-psi_SSDP.pdf
*
* @author Ziver
*
* ********* Message clarification:
* ****** Incoming:
* ST: Search Target, this is object of the discovery request, (e.g., ssdp:all, etc.)
* HOST: This is the SSDP multicast address
* MAN: Description of packet type, (e.g., "ssdp:discover", )
* MX: Wait these few seconds and then send response
*
* ****** Outgoing:
* EXT: required by HTTP - not used with SSDP
* SERVER: informational
* LOCATION: This is the URL to request the QueryEndpointsInterface endpoint
* USN: advertisement UUID
* CACHE-CONTROL: max-age = seconds until advertisement expires
* NT: Notify target same as ST
* NTS: same as Man but for Notify messages
*/
public class SSDPServer extends ThreadedUDPNetwork implements ThreadedUDPNetworkThread{
private static final Logger logger = LogUtil.getLogger();
public static final String SERVER_INFO = "Zutil SSDP Java Server";
public static final int DEFAULT_CACHE_TIME = 60*30; // 30 min
public static final int BUFFER_SIZE = 512;
public static final String SSDP_MULTICAST_ADDR = "239.255.255.250";
public static final int SSDP_PORT = 1900;
// instance specific values
private int cache_time;
private NotifyTimer notifyTimer = null;
/** HashMap that contains services as < SearchTargetName, SSDPServiceInfo > */
private HashMap<String, SSDPServiceInfo> services;
public SSDPServer() throws IOException{
super( null, SSDP_MULTICAST_ADDR, SSDP_PORT );
super.setThread( this );
services = new HashMap<String, SSDPServiceInfo>();
setChacheTime( DEFAULT_CACHE_TIME );
enableNotify( true );
}
/**
* Adds an service that will be announced.
*
* @param service add a new service to be announced
*/
public void addService(SSDPServiceInfo service){
services.put( service.getSearchTarget(), service );
}
/**
* Remove a service from being announced. This function will
* send out an byebye message to the clients that the service is down.
*
* @param searchTarget is the ST value in SSDP
*/
public void removeService(String searchTarget){
sendByeBye( searchTarget );
services.remove( searchTarget );
}
/**
* Sets the cache time that will be sent to
* the clients. If notification is enabled then an
* notification message will be sent every cache_time/2 seconds
*
* @param time is the time in seconds
*/
public void setChacheTime(int time){
cache_time = time;
if( isNotifyEnabled() ){
enableNotify(false);
enableNotify(true);
}
}
/**
* Enable or disable notification messages to clients
* every cache_time/2 seconds
*/
public void enableNotify(boolean enable){
if( enable && notifyTimer==null ){
notifyTimer = new NotifyTimer();
Timer timer = new Timer();
timer.schedule(new NotifyTimer(), 0, cache_time*1000/2);
}else if( !enable && notifyTimer!=null ){
notifyTimer.cancel();
notifyTimer = null;
}
}
/**
* @return if notification messages is enabled
*/
public boolean isNotifyEnabled(){
return notifyTimer != null;
}
/**
* Handles the incoming packets like this:
*
* ***** REQUEST:
* M-SEARCH * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* Man: "ssdp:discover"
* ST: ge:fridge
* MX: 3
*
* ***** RESPONSE;
* HTTP/1.1 200 OK
* Ext:
* Cache-Control: no-cache="Ext", max-age = 5000
* ST: ge:fridge
* USN: uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6
* Location: http://localhost:80
*
*/
public void receivedPacket(DatagramPacket packet, ThreadedUDPNetwork network) {
try {
String msg = new String( packet.getData(), packet.getOffset(), packet.getLength() );
HttpHeaderParser headerParser = new HttpHeaderParser( msg );
HttpHeader header = headerParser.read();
// ******* Respond
// Check that the message is an ssdp discovery message
if( header.getRequestType() != null && header.getRequestType().equalsIgnoreCase("M-SEARCH") ){
String man = header.getHeader("Man");
if(man != null)
man = StringUtil.trim(man, '\"');
String st = header.getHeader("ST");
// Check that its the correct URL and that its an ssdp:discover message
if( header.getRequestURL().equals("*") && "ssdp:discover".equalsIgnoreCase(man) ){
// Check if the requested service exists
if( services.containsKey( st ) ){
logger.log(Level.FINEST, "Received Multicast(from: "+packet.getAddress()+"): "+ header);
// Generate the SSDP response
StringOutputStream response = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( response );
http.setStatusCode(200);
http.setHeader("Location", services.get(st).getLocation() );
http.setHeader("USN", services.get(st).getUSN() );
http.setHeader("Server", SERVER_INFO );
http.setHeader("ST", st );
http.setHeader("EXT", "" );
http.setHeader("Cache-Control", "max-age = "+ cache_time );
if(services.get(st) instanceof SSDPCustomInfo)
((SSDPCustomInfo)services.get(st)).writeHeaders(http);
logger.log(Level.FINEST, "Sending Response: "+ http);
http.flush();
String strData = response.toString();
byte[] data = strData.getBytes();
packet = new DatagramPacket(
data, data.length,
packet.getAddress(),
packet.getPort());
network.send( packet );
http.close();
}
}
}
} catch (IOException e) {
logger.log(Level.SEVERE, null, e);
}
}
/**
* This thread is a timer task that sends an
* notification message to the network every
* cache_time/2 seconds.
*
* @author Ziver
*/
private class NotifyTimer extends TimerTask {
public void run(){
sendNotify();
}
}
/**
* Sends keep-alive messages to update the cache of the clients
*/
public void sendNotify(){
for(String st : services.keySet()){
sendNotify( st );
}
}
/**
* Sends an keepalive message to update the cache of the clients
*
* @param searchTarget is the ST value of the service
*
* ********** Message ex:
* NOTIFY * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* NT: blenderassociation:blender
* NTS: ssdp:alive
* USN: someunique:idscheme3
* Location: http://localhost:80
* Cache-Control: max-age = 7393
*/
public void sendNotify(String searchTarget){
try {
SSDPServiceInfo service = services.get(searchTarget);
// Generate the SSDP response
StringOutputStream msg = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HttpMessageType.REQUEST );
http.setRequestType("NOTIFY");
http.setRequestURL("*");
http.setHeader("Server", SERVER_INFO );
http.setHeader("Host", SSDP_MULTICAST_ADDR+":"+SSDP_PORT );
http.setHeader("NT", searchTarget );
http.setHeader("NTS", "ssdp:alive" );
http.setHeader("Location", service.getLocation() );
http.setHeader("Cache-Control", "max-age = "+cache_time );
http.setHeader("USN", service.getUSN() );
if(service instanceof SSDPCustomInfo)
((SSDPCustomInfo) service).writeHeaders(http);
logger.log(Level.FINEST, "Sending Notification: " + http);
http.flush();
byte[] data = msg.toString().getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length,
InetAddress.getByName( SSDP_MULTICAST_ADDR ),
SSDP_PORT );
super.send( packet );
http.close();
} catch (Exception e) {
logger.log(Level.SEVERE, null, e);
}
}
/**
* Shutdown message is sent to the clients that all
* the service is shutting down.
*/
public void sendByeBye(){
for(String st : services.keySet()){
sendByeBye( st );
}
}
/**
* Shutdown message is sent to the clients that the service is shutting down
*
* @param searchTarget is the ST value of the service
*
* ********** Message ex:
* NOTIFY * HTTP/1.1
* Host: 239.255.255.250:reservedSSDPport
* NT: someunique:idscheme3
* NTS: ssdp:byebye
* USN: someunique:idscheme3
*/
public void sendByeBye(String searchTarget){
try {
// Generate the SSDP response
StringOutputStream msg = new StringOutputStream();
HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HttpMessageType.REQUEST );
http.setRequestType("NOTIFY");
http.setRequestURL("*");
http.setHeader("Server", SERVER_INFO );
http.setHeader("Host", SSDP_MULTICAST_ADDR+":"+SSDP_PORT );
http.setHeader("NT", searchTarget );
http.setHeader("NTS", "ssdp:byebye" );
http.setHeader("USN", services.get(searchTarget).getUSN() );
logger.log(Level.FINEST, "Sending ByeBye: " + http);
http.flush();
byte[] data = msg.toString().getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length,
InetAddress.getByName( SSDP_MULTICAST_ADDR ),
SSDP_PORT );
super.send( packet );
http.close();
} catch (Exception e) {
logger.log(Level.SEVERE, null, e);
}
}
}

View file

@ -24,8 +24,6 @@
package zutil.net.ssdp;
import java.net.InetAddress;
/**
* This class contains information about a service from
* or through the SSDP protocol

View file

@ -1,161 +1,164 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPrintStream;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;
/**
* This class contains information about a service from
* or through the SSDP protocol
*
* @author Ziver
*/
public class StandardSSDPInfo implements SSDPServiceInfo, SSDPCustomInfo{
private String location;
private String st;
private String usn;
private long expiration_time;
// All header parameters
private HashMap<String, String> headers = new HashMap<>();
private InetAddress inetAddress;
/**
* @param l is the value to set the Location variable
*/
public void setLocation(String l) {
location = l;
}
/**
* @param st is the value to set the SearchTarget variable
*/
public void setST(String st) {
this.st = st;
}
/**
* @param usn is the value to set the USN variable
*/
protected void setUSN(String usn) {
this.usn = usn;
}
/**
* @param time sets the expiration time of values in this object
*/
protected void setExpirationTime(long time) {
expiration_time = time;
}
/**
* @return The URL to the Service, e.g. "http://192.168.0.1:80/index.html"
*/
public String getLocation(){
return location;
}
/**
* @return the Search Target, e.g. "upnp:rootdevice"
*/
public String getSearchTarget(){
return st;
}
/**
* @return the expiration time for the values in this object
*/
public long getExpirationTime(){
return expiration_time;
}
/**
* @return the USN value, e.g. "uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6 "
*/
public String getUSN(){
if( usn==null )
usn = genUSN();
return usn+"::"+st;
}
/**
* @return only the USN UUID String
*/
public String getUUID(){
if( usn==null )
usn = genUSN();
return usn;
}
/**
* Generates an unique USN for the service
*
* @return an unique string that corresponds to the service
*/
private String genUSN(){
return "uuid:" + UUID.nameUUIDFromBytes( (st+location+Math.random()).getBytes() );
}
public String toString(){
return "USN: "+usn+"\nLocation: "+location+"\nST: "+st+"\nExpiration-Time: "+new Date(expiration_time);
}
public void setHeader(String key, String value) {
headers.put(key, value);
}
public String getHeader(String header){
return headers.get(header);
}
@Override
public void writeHeaders(HttpPrintStream http) {
try {
for (String key : headers.keySet())
http.setHeader(key, headers.get(key));
}catch(IOException e){
e.printStackTrace();
}
}
@Override
public void readHeaders(HttpHeaderParser http) {
HashMap<String,String> httpHeaders = http.getHeaders();
for (String key : httpHeaders.keySet())
headers.put(key, httpHeaders.get(key));
}
public InetAddress getInetAddress(){
return inetAddress;
}
protected void setInetAddress(InetAddress inetAddress) {
this.inetAddress = inetAddress;
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.ssdp;
import zutil.net.http.HttpHeader;
import zutil.net.http.HttpPrintStream;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.UUID;
/**
* This class contains information about a service from
* or through the SSDP protocol
*
* @author Ziver
*/
public class StandardSSDPInfo implements SSDPServiceInfo, SSDPCustomInfo{
private String location;
private String st;
private String usn;
private long expiration_time;
// All header parameters
private HashMap<String, String> headers = new HashMap<>();
private InetAddress inetAddress;
/**
* @param l is the value to set the Location variable
*/
public void setLocation(String l) {
location = l;
}
/**
* @param st is the value to set the SearchTarget variable
*/
public void setST(String st) {
this.st = st;
}
/**
* @param usn is the value to set the USN variable
*/
protected void setUSN(String usn) {
this.usn = usn;
}
/**
* @param time sets the expiration time of values in this object
*/
protected void setExpirationTime(long time) {
expiration_time = time;
}
/**
* @return The URL to the Service, e.g. "http://192.168.0.1:80/index.html"
*/
public String getLocation(){
return location;
}
/**
* @return the Search Target, e.g. "upnp:rootdevice"
*/
public String getSearchTarget(){
return st;
}
/**
* @return the expiration time for the values in this object
*/
public long getExpirationTime(){
return expiration_time;
}
/**
* @return the USN value, e.g. "uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6 "
*/
public String getUSN(){
if( usn==null )
usn = genUSN();
return usn+"::"+st;
}
/**
* @return only the USN UUID String
*/
public String getUUID(){
if( usn==null )
usn = genUSN();
return usn;
}
/**
* Generates an unique USN for the service
*
* @return an unique string that corresponds to the service
*/
private String genUSN(){
return "uuid:" + UUID.nameUUIDFromBytes( (st+location+Math.random()).getBytes() );
}
public String toString(){
return "USN: "+usn+"\nLocation: "+location+"\nST: "+st+"\nExpiration-Time: "+new Date(expiration_time);
}
public void setHeader(String key, String value) {
headers.put(key, value);
}
public String getHeader(String header){
return headers.get(header);
}
@Override
public void writeHeaders(HttpPrintStream http) {
try {
for (String key : headers.keySet())
http.setHeader(key, headers.get(key));
}catch(IOException e){
e.printStackTrace();
}
}
@Override
public void readHeaders(HttpHeader header) {
Iterator<String> it = header.getHeaderKeys();
while (it.hasNext()) {
String key = it.next();
headers.put(key, header.getHeader(key));
}
}
public InetAddress getInetAddress(){
return inetAddress;
}
protected void setInetAddress(InetAddress inetAddress) {
this.inetAddress = inetAddress;
}
}