first commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
Type 'make' to build the packet decoder, 'hairtunes'.
|
||||
You need the following installed:
|
||||
|
||||
openssl
|
||||
libao
|
||||
avahi (avahi-daemon running and avahi-publish-service on path)
|
||||
Perl
|
||||
|
||||
Perl modules (install from CPAN):
|
||||
HTTP::Message
|
||||
Crypt::OpenSSL::RSA
|
||||
@@ -0,0 +1,4 @@
|
||||
CFLAGS = `pkg-config --cflags --libs ao openssl`
|
||||
|
||||
hairtunes: hairtunes.c alac.c
|
||||
gcc hairtunes.c alac.c -D__i386 -lm $(CFLAGS) -o hairtunes
|
||||
@@ -0,0 +1,34 @@
|
||||
Shairport v0.01
|
||||
==============
|
||||
James Laird <jhl@mafipulation.org>
|
||||
April 5, 2011
|
||||
|
||||
What it is
|
||||
----------
|
||||
This program emulates an Airport Express for the purpose of streaming music from
|
||||
iTunes and compatible iPods. It implements a server for the Apple RAOP protocol.
|
||||
|
||||
It probably supports multiple simultaneous streams, if your audio output chain
|
||||
(as detected by libao) does so.
|
||||
|
||||
How to use it
|
||||
-------------
|
||||
1. Make sure avahi-daemon is running and the prerequisites are installed (see
|
||||
INSTALL).
|
||||
2. Edit shairport.pl with your favourite text editor to set the access
|
||||
point name and/or password, if desired.
|
||||
3. `perl shairport.pl`
|
||||
|
||||
The triangle-in-rectangle Airtunes logo will appear in the iTunes status bar of
|
||||
any machine on the network, or on iPod play controls screen. Choose your access
|
||||
point name to start streaming to the Shairport instance.
|
||||
|
||||
Thanks
|
||||
------
|
||||
Big thanks to David Hammerton for releasing an ALAC decoder, which is reproduced
|
||||
here in full.
|
||||
Thanks to everyone who has worked to reverse engineer the RAOP protocol - after
|
||||
finding the keys, everything else was pretty much trivial.
|
||||
Thanks also to Apple for obfuscating the private key in the ROM image, using a
|
||||
scheme that made the deobfuscation code itself stand out like a flare.
|
||||
Thanks to wtbw.
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef __ALAC__DECOMP_H
|
||||
#define __ALAC__DECOMP_H
|
||||
|
||||
typedef struct alac_file alac_file;
|
||||
|
||||
alac_file *create_alac(int samplesize, int numchannels);
|
||||
void decode_frame(alac_file *alac,
|
||||
unsigned char *inbuffer,
|
||||
void *outbuffer, int *outputsize);
|
||||
void alac_set_info(alac_file *alac, char *inputbuffer);
|
||||
void allocate_buffers(alac_file *alac);
|
||||
|
||||
struct alac_file
|
||||
{
|
||||
unsigned char *input_buffer;
|
||||
int input_buffer_bitaccumulator; /* used so we can do arbitary
|
||||
bit reads */
|
||||
|
||||
int samplesize;
|
||||
int numchannels;
|
||||
int bytespersample;
|
||||
|
||||
|
||||
/* buffers */
|
||||
int32_t *predicterror_buffer_a;
|
||||
int32_t *predicterror_buffer_b;
|
||||
|
||||
int32_t *outputsamples_buffer_a;
|
||||
int32_t *outputsamples_buffer_b;
|
||||
|
||||
int32_t *uncompressed_bytes_buffer_a;
|
||||
int32_t *uncompressed_bytes_buffer_b;
|
||||
|
||||
|
||||
|
||||
/* stuff from setinfo */
|
||||
uint32_t setinfo_max_samples_per_frame; /* 0x1000 = 4096 */ /* max samples per frame? */
|
||||
uint8_t setinfo_7a; /* 0x00 */
|
||||
uint8_t setinfo_sample_size; /* 0x10 */
|
||||
uint8_t setinfo_rice_historymult; /* 0x28 */
|
||||
uint8_t setinfo_rice_initialhistory; /* 0x0a */
|
||||
uint8_t setinfo_rice_kmodifier; /* 0x0e */
|
||||
uint8_t setinfo_7f; /* 0x02 */
|
||||
uint16_t setinfo_80; /* 0x00ff */
|
||||
uint32_t setinfo_82; /* 0x000020e7 */ /* max sample size?? */
|
||||
uint32_t setinfo_86; /* 0x00069fe4 */ /* bit rate (avarge)?? */
|
||||
uint32_t setinfo_8a_rate; /* 0x0000ac44 */
|
||||
/* end setinfo stuff */
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif /* __ALAC__DECOMP_H */
|
||||
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
/*
|
||||
* HairTunes - RAOP packet handler and slave-clocked replay engine
|
||||
* Copyright (c) James Laird 2011
|
||||
* All rights reserved.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <openssl/aes.h>
|
||||
#include <ao/ao.h>
|
||||
#include <math.h>
|
||||
|
||||
#ifdef FANCY_RESAMPLING
|
||||
#include <samplerate.h>
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
int debug = 0;
|
||||
|
||||
#include "alac.h"
|
||||
|
||||
// default buffer - about half a second
|
||||
#define BUFFER_FRAMES 64
|
||||
#define START_FILL 55
|
||||
#define MAX_PACKET 2048
|
||||
|
||||
typedef unsigned short seq_t;
|
||||
|
||||
// global options (constant after init)
|
||||
unsigned char aeskey[16], aesiv[16];
|
||||
AES_KEY aes;
|
||||
char *rtphost = 0;
|
||||
int dataport = 0, controlport = 0, timingport = 0;
|
||||
int fmtp[32];
|
||||
int sampling_rate;
|
||||
int frame_size;
|
||||
#define FRAME_BYTES (4*frame_size)
|
||||
// maximal resampling shift - conservative
|
||||
#define OUTFRAME_BYTES (4*(frame_size+3))
|
||||
|
||||
|
||||
alac_file *decoder_info;
|
||||
|
||||
#ifdef FANCY_RESAMPLING
|
||||
int fancy_resampling = 1;
|
||||
SRC_STATE *src;
|
||||
#endif
|
||||
|
||||
void rtp_request_resend(seq_t first, seq_t last);
|
||||
void init_buffer(void);
|
||||
void ab_resync(void);
|
||||
|
||||
// interthread variables
|
||||
// stdin->decoder
|
||||
volatile double volume = 1.0;
|
||||
volatile long fix_volume = 0x10000;
|
||||
|
||||
typedef struct audio_buffer_entry { // decoded audio packets
|
||||
int ready;
|
||||
signed short *data;
|
||||
} abuf_t;
|
||||
volatile abuf_t audio_buffer[BUFFER_FRAMES];
|
||||
#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES)
|
||||
|
||||
// mutex-protected variables
|
||||
volatile seq_t ab_read, ab_write;
|
||||
int ab_buffering = 1, ab_synced = 0;
|
||||
pthread_mutex_t ab_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
pthread_cond_t ab_buffer_ready = PTHREAD_COND_INITIALIZER;
|
||||
|
||||
void die(char *why) {
|
||||
fprintf(stderr, "FATAL: %s\n", why);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int hex2bin(unsigned char *buf, char *hex) {
|
||||
int i, j;
|
||||
if (strlen(hex) != 0x20)
|
||||
return 1;
|
||||
for (i=0; i<0x10; i++) {
|
||||
if (!sscanf(hex, "%2X", &j))
|
||||
return 1;
|
||||
hex += 2;
|
||||
*buf++ = j;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init_decoder(void) {
|
||||
alac_file *alac;
|
||||
|
||||
frame_size = fmtp[1]; // stereo samples
|
||||
sampling_rate = fmtp[11];
|
||||
|
||||
int sample_size = fmtp[3];
|
||||
if (sample_size != 16)
|
||||
die("only 16-bit samples supported!");
|
||||
|
||||
alac = create_alac(sample_size, 2);
|
||||
if (!alac)
|
||||
return 1;
|
||||
decoder_info = alac;
|
||||
|
||||
alac->setinfo_max_samples_per_frame = frame_size;
|
||||
alac->setinfo_7a = fmtp[2];
|
||||
alac->setinfo_sample_size = sample_size;
|
||||
alac->setinfo_rice_historymult = fmtp[4];
|
||||
alac->setinfo_rice_initialhistory = fmtp[5];
|
||||
alac->setinfo_rice_kmodifier = fmtp[6];
|
||||
alac->setinfo_7f = fmtp[7];
|
||||
alac->setinfo_80 = fmtp[8];
|
||||
alac->setinfo_82 = fmtp[9];
|
||||
alac->setinfo_86 = fmtp[10];
|
||||
alac->setinfo_8a_rate = fmtp[11];
|
||||
allocate_buffers(alac);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char *hexaeskey = 0, *hexaesiv = 0;
|
||||
char *fmtpstr = 0;
|
||||
char *arg;
|
||||
int i;
|
||||
assert(RAND_MAX >= 0x10000); // XXX move this to compile time
|
||||
while (arg = *++argv) {
|
||||
if (!strcasecmp(arg, "iv")) {
|
||||
hexaesiv = *++argv;
|
||||
argc--;
|
||||
} else
|
||||
if (!strcasecmp(arg, "key")) {
|
||||
hexaeskey = *++argv;
|
||||
argc--;
|
||||
} else
|
||||
if (!strcasecmp(arg, "fmtp")) {
|
||||
fmtpstr = *++argv;
|
||||
} else
|
||||
if (!strcasecmp(arg, "cport")) {
|
||||
controlport = atoi(*++argv);
|
||||
} else
|
||||
if (!strcasecmp(arg, "tport")) {
|
||||
timingport = atoi(*++argv);
|
||||
} else
|
||||
if (!strcasecmp(arg, "dport")) {
|
||||
dataport = atoi(*++argv);
|
||||
} else
|
||||
if (!strcasecmp(arg, "host")) {
|
||||
rtphost = *++argv;
|
||||
}
|
||||
#ifdef FANCY_RESAMPLING
|
||||
else
|
||||
if (!strcasecmp(arg, "resamp")) {
|
||||
fancy_resampling = atoi(*++argv);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!hexaeskey || !hexaesiv)
|
||||
die("Must supply AES key and IV!");
|
||||
|
||||
if (hex2bin(aesiv, hexaesiv))
|
||||
die("can't understand IV");
|
||||
if (hex2bin(aeskey, hexaeskey))
|
||||
die("can't understand key");
|
||||
AES_set_decrypt_key(aeskey, 128, &aes);
|
||||
|
||||
memset(fmtp, 0, sizeof(fmtp));
|
||||
i = 0;
|
||||
while (arg = strsep(&fmtpstr, " \t"))
|
||||
fmtp[i++] = atoi(arg);
|
||||
|
||||
init_decoder();
|
||||
init_buffer();
|
||||
init_rtp(); // open a UDP listen port and start a listener; decode into ring buffer
|
||||
fflush(stdout);
|
||||
init_output(); // resample and output from ring buffer
|
||||
|
||||
char line[128];
|
||||
int in_line = 0;
|
||||
int n;
|
||||
double f;
|
||||
while (fgets(line + in_line, sizeof(line) - in_line, stdin)) {
|
||||
n = strlen(line);
|
||||
if (line[n-1] != '\n') {
|
||||
in_line = strlen(line) - 1;
|
||||
if (n == sizeof(line)-1)
|
||||
in_line = 0;
|
||||
continue;
|
||||
}
|
||||
if (sscanf(line, "vol: %lf\n", &f)) {
|
||||
assert(f<=0);
|
||||
if (debug)
|
||||
fprintf(stderr, "VOL: %lf\n", f);
|
||||
volume = pow(10.0,0.1*f);
|
||||
fix_volume = 65536.0 * volume;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(line, "exit\n")) {
|
||||
exit(0);
|
||||
}
|
||||
if (!strcmp(line, "flush\n")) {
|
||||
pthread_mutex_lock(&ab_mutex);
|
||||
ab_resync();
|
||||
pthread_mutex_unlock(&ab_mutex);
|
||||
if (debug)
|
||||
fprintf(stderr, "FLUSH\n");
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "bye!\n");
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
void init_buffer(void) {
|
||||
int i;
|
||||
for (i=0; i<BUFFER_FRAMES; i++)
|
||||
audio_buffer[i].data = malloc(OUTFRAME_BYTES);
|
||||
ab_resync();
|
||||
}
|
||||
|
||||
void ab_resync(void) {
|
||||
int i;
|
||||
for (i=0; i<BUFFER_FRAMES; i++)
|
||||
audio_buffer[i].ready = 0;
|
||||
ab_synced = 0;
|
||||
}
|
||||
|
||||
// the sequence numbers will wrap pretty often.
|
||||
// this returns true if the second arg is after the first
|
||||
static inline int seq_order(seq_t a, seq_t b) {
|
||||
signed short d = b - a;
|
||||
return d > 0;
|
||||
}
|
||||
|
||||
void alac_decode(short *dest, char *buf, int len) {
|
||||
char packet[MAX_PACKET];
|
||||
assert(len<=MAX_PACKET);
|
||||
|
||||
char iv[16];
|
||||
int i;
|
||||
memcpy(iv, aesiv, sizeof(iv));
|
||||
for (i=0; i+16<=len; i += 16)
|
||||
AES_cbc_encrypt(buf+i, packet+i, 0x10, &aes, iv, AES_DECRYPT);
|
||||
if (len & 0xf)
|
||||
memcpy(packet+i, buf+i, len & 0xf);
|
||||
|
||||
int outsize;
|
||||
|
||||
decode_frame(decoder_info, packet, dest, &outsize);
|
||||
|
||||
assert(outsize == FRAME_BYTES);
|
||||
}
|
||||
|
||||
void buffer_put_packet(seq_t seqno, char *data, int len) {
|
||||
volatile abuf_t *abuf = 0;
|
||||
short read;
|
||||
short buf_fill;
|
||||
|
||||
pthread_mutex_lock(&ab_mutex);
|
||||
if (!ab_synced) {
|
||||
ab_write = seqno;
|
||||
ab_read = seqno-1;
|
||||
ab_synced = 1;
|
||||
}
|
||||
if (seqno == ab_write+1) { // expected packet
|
||||
abuf = audio_buffer + BUFIDX(seqno);
|
||||
ab_write = seqno;
|
||||
} else if (seq_order(ab_write, seqno)) { // newer than expected
|
||||
rtp_request_resend(ab_write, seqno-1);
|
||||
abuf = audio_buffer + BUFIDX(seqno);
|
||||
ab_write = seqno;
|
||||
} else if (seq_order(ab_read, seqno)) { // late but not yet played
|
||||
abuf = audio_buffer + BUFIDX(seqno);
|
||||
} else { // too late.
|
||||
fprintf(stderr, "\nlate packet %04X (%04X:%04X)\n", seqno, ab_read, ab_write);
|
||||
}
|
||||
buf_fill = ab_write - ab_read;
|
||||
pthread_mutex_unlock(&ab_mutex);
|
||||
|
||||
if (abuf) {
|
||||
alac_decode(abuf->data, data, len);
|
||||
abuf->ready = 1;
|
||||
}
|
||||
|
||||
if (ab_buffering && buf_fill >= START_FILL)
|
||||
pthread_cond_signal(&ab_buffer_ready);
|
||||
if (!ab_buffering) {
|
||||
// check if the t+10th packet has arrived... last-chance resend
|
||||
read = ab_read + 10;
|
||||
abuf = audio_buffer + BUFIDX(read);
|
||||
if (!abuf->ready)
|
||||
rtp_request_resend(read, read);
|
||||
}
|
||||
}
|
||||
|
||||
static int rtp_sockets[2]; // data, control
|
||||
struct sockaddr_in rtp_client;
|
||||
void *rtp_thread_func(void *arg) {
|
||||
socklen_t si_len = sizeof(rtp_client);
|
||||
char packet[MAX_PACKET];
|
||||
char *pktp;
|
||||
seq_t seqno;
|
||||
ssize_t plen;
|
||||
int sock = rtp_sockets[0], csock = rtp_sockets[1];
|
||||
int readsock;
|
||||
char type;
|
||||
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(sock, &fds);
|
||||
FD_SET(csock, &fds);
|
||||
|
||||
while (select(csock>sock ? csock+1 : sock+1, &fds, 0, 0, 0)!=-1) {
|
||||
if (FD_ISSET(sock, &fds)) {
|
||||
readsock = sock;
|
||||
} else {
|
||||
readsock = csock;
|
||||
}
|
||||
FD_SET(sock, &fds);
|
||||
FD_SET(csock, &fds);
|
||||
|
||||
plen = recvfrom(readsock, packet, sizeof(packet), 0, (struct sockaddr*)&rtp_client, &si_len);
|
||||
if (plen < 0)
|
||||
continue;
|
||||
assert(plen<=MAX_PACKET);
|
||||
|
||||
type = packet[1] & ~0x80;
|
||||
if (type == 0x60 || type == 0x56) { // audio data / resend
|
||||
pktp = packet;
|
||||
if (type==0x56) {
|
||||
pktp += 4;
|
||||
plen -= 4;
|
||||
}
|
||||
seqno = ntohs(*(unsigned short *)(pktp+2));
|
||||
buffer_put_packet(seqno, pktp+12, plen-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rtp_request_resend(seq_t first, seq_t last) {
|
||||
if (seq_order(last, first))
|
||||
return;
|
||||
|
||||
fprintf(stderr, "requesting resend on %d packets (port %d)\n", last-first+1, controlport);
|
||||
|
||||
char req[8]; // *not* a standard RTCP NACK
|
||||
req[0] = 0x80;
|
||||
req[1] = 0x55|0x80; // Apple 'resend'
|
||||
*(unsigned short *)(req+2) = htons(1); // our seqnum
|
||||
*(unsigned short *)(req+4) = htons(first); // missed seqnum
|
||||
*(unsigned short *)(req+6) = htons(last-first+1); // count
|
||||
|
||||
rtp_client.sin_port = htons(controlport);
|
||||
sendto(rtp_sockets[1], req, sizeof(req), 0, (struct sockaddr *)&rtp_client, sizeof(struct sockaddr_in));
|
||||
}
|
||||
|
||||
|
||||
int init_rtp(void) {
|
||||
struct sockaddr_in si;
|
||||
int sock, csock; // data and control (we treat the streams the same here)
|
||||
|
||||
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (sock==-1)
|
||||
die("Can't create socket!");
|
||||
|
||||
memset(&si, 0, sizeof(si));
|
||||
si.sin_family = AF_INET;
|
||||
si.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
|
||||
unsigned short port = 6000 - 3;
|
||||
do {
|
||||
port += 3;
|
||||
si.sin_port = htons(port);
|
||||
} while (bind(sock, (struct sockaddr*)&si, sizeof(si))==-1);
|
||||
|
||||
csock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (csock==-1)
|
||||
die("Can't create socket!");
|
||||
si.sin_port = htons(port + 1);
|
||||
if (bind(csock, (struct sockaddr*)&si, sizeof(si))==-1)
|
||||
die("can't bind control socket");
|
||||
|
||||
printf("port: %d\n", port); // let our handler know where we end up listening
|
||||
printf("cport: %d\n", port+1);
|
||||
|
||||
pthread_t rtp_thread;
|
||||
rtp_sockets[0] = sock;
|
||||
rtp_sockets[1] = csock;
|
||||
pthread_create(&rtp_thread, NULL, rtp_thread_func, (void *)rtp_sockets);
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
static inline short dithered_vol(short sample) {
|
||||
static short rand_a, rand_b;
|
||||
long out;
|
||||
rand_b = rand_a;
|
||||
rand_a = rand() & 0xffff;
|
||||
|
||||
out = (long)sample * fix_volume;
|
||||
if (fix_volume < 0x10000) {
|
||||
out += rand_a;
|
||||
out -= rand_b;
|
||||
}
|
||||
return out>>16;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
double hist[2];
|
||||
double a[2];
|
||||
double b[3];
|
||||
} biquad_t;
|
||||
|
||||
static void biquad_init(biquad_t *bq, double a[], double b[]) {
|
||||
bq->hist[0] = bq->hist[1] = 0.0;
|
||||
memcpy(bq->a, a, 2*sizeof(double));
|
||||
memcpy(bq->b, b, 3*sizeof(double));
|
||||
}
|
||||
|
||||
static void biquad_lpf(biquad_t *bq, double freq, double Q) {
|
||||
double w0 = 2*M_PI*freq/((float)sampling_rate/(float)frame_size);
|
||||
double alpha = sin(w0)/(2.0*Q);
|
||||
|
||||
double a_0 = 1.0 + alpha;
|
||||
double b[3], a[2];
|
||||
b[0] = (1.0-cos(w0))/(2.0*a_0);
|
||||
b[1] = (1.0-cos(w0))/a_0;
|
||||
b[2] = b[0];
|
||||
a[0] = -2.0*cos(w0)/a_0;
|
||||
a[1] = (1-alpha)/a_0;
|
||||
|
||||
biquad_init(bq, a, b);
|
||||
}
|
||||
|
||||
static double biquad_filt(biquad_t *bq, double in) {
|
||||
double w = in - bq->a[0]*bq->hist[0] - bq->a[1]*bq->hist[1];
|
||||
double out = bq->b[1]*bq->hist[0] + bq->b[2]*bq->hist[1] + bq->b[0]*w;
|
||||
bq->hist[1] = bq->hist[0];
|
||||
bq->hist[0] = w;
|
||||
}
|
||||
|
||||
double bf_playback_rate = 1.0;
|
||||
|
||||
static double bf_est_drift = 0.0; // local clock is slower by
|
||||
static biquad_t bf_drift_lpf;
|
||||
static double bf_est_err = 0.0, bf_last_err;
|
||||
static biquad_t bf_err_lpf, bf_err_deriv_lpf;
|
||||
static double desired_fill;
|
||||
static int fill_count;
|
||||
|
||||
void bf_est_reset(short fill) {
|
||||
biquad_lpf(&bf_drift_lpf, 1.0/180.0, 0.3);
|
||||
biquad_lpf(&bf_err_lpf, 1.0/10.0, 0.25);
|
||||
biquad_lpf(&bf_err_deriv_lpf, 1.0/2.0, 0.2);
|
||||
fill_count = 0;
|
||||
bf_playback_rate = 1.0;
|
||||
bf_est_err = bf_last_err = 0;
|
||||
desired_fill = fill_count = 0;
|
||||
}
|
||||
void bf_est_update(short fill) {
|
||||
if (fill_count < 1000) {
|
||||
desired_fill += (double)fill/1000.0;
|
||||
fill_count++;
|
||||
return;
|
||||
}
|
||||
|
||||
#define CONTROL_A (1e-4)
|
||||
#define CONTROL_B (1e-1)
|
||||
|
||||
double buf_delta = fill - desired_fill;
|
||||
bf_est_err = biquad_filt(&bf_err_lpf, buf_delta);
|
||||
double err_deriv = biquad_filt(&bf_err_deriv_lpf, bf_est_err - bf_last_err);
|
||||
|
||||
bf_est_drift = biquad_filt(&bf_drift_lpf, CONTROL_B*(bf_est_err*CONTROL_A + err_deriv) + bf_est_drift);
|
||||
|
||||
if (debug)
|
||||
fprintf(stderr, "bf %d err %f drift %f desiring %f ed %f estd %f\r", fill, bf_est_err, bf_est_drift, desired_fill, err_deriv, err_deriv + CONTROL_A*bf_est_err);
|
||||
bf_playback_rate = 1.0 + CONTROL_A*bf_est_err + bf_est_drift;
|
||||
|
||||
bf_last_err = bf_est_err;
|
||||
}
|
||||
|
||||
// get the next frame, when available. return 0 if underrun/stream reset.
|
||||
short *buffer_get_frame(void) {
|
||||
short buf_fill;
|
||||
seq_t read;
|
||||
|
||||
pthread_mutex_lock(&ab_mutex);
|
||||
|
||||
buf_fill = ab_write - ab_read;
|
||||
if (buf_fill < 1 || !ab_synced) { // init or underrun. stop and wait
|
||||
if (ab_synced)
|
||||
fprintf(stderr, "\nunderrun.\n");
|
||||
|
||||
ab_buffering = 1;
|
||||
pthread_cond_wait(&ab_buffer_ready, &ab_mutex);
|
||||
ab_read++;
|
||||
buf_fill = ab_write - ab_read;
|
||||
pthread_mutex_unlock(&ab_mutex);
|
||||
|
||||
bf_est_reset(buf_fill);
|
||||
return 0;
|
||||
}
|
||||
if (buf_fill >= BUFFER_FRAMES) { // overrunning! uh-oh. restart at a sane distance
|
||||
fprintf(stderr, "\noverrun.\n");
|
||||
ab_read = ab_write - START_FILL;
|
||||
}
|
||||
read = ab_read;
|
||||
ab_read++;
|
||||
pthread_mutex_unlock(&ab_mutex);
|
||||
|
||||
buf_fill = ab_write - ab_read;
|
||||
bf_est_update(buf_fill);
|
||||
|
||||
volatile abuf_t *curframe = audio_buffer + BUFIDX(read);
|
||||
if (!curframe->ready) {
|
||||
fprintf(stderr, "\nmissing frame.\n");
|
||||
memset(curframe->data, 0, FRAME_BYTES);
|
||||
}
|
||||
curframe->ready = 0;
|
||||
return curframe->data;
|
||||
}
|
||||
|
||||
int stuff_buffer(double playback_rate, short *inptr, short *outptr) {
|
||||
int i;
|
||||
int stuffsamp = frame_size;
|
||||
int stuff = 0;
|
||||
double p_stuff;
|
||||
|
||||
p_stuff = 1.0 - pow(1.0 - fabs(playback_rate-1.0), frame_size);
|
||||
|
||||
if ((float)rand()/((float)RAND_MAX) < p_stuff) {
|
||||
stuff = playback_rate > 1.0 ? -1 : 1;
|
||||
stuffsamp = rand() % (frame_size - 1);
|
||||
}
|
||||
|
||||
for (i=0; i<stuffsamp; i++) { // the whole frame, if no stuffing
|
||||
*outptr++ = dithered_vol(*inptr++);
|
||||
*outptr++ = dithered_vol(*inptr++);
|
||||
};
|
||||
if (stuff) {
|
||||
if (stuff==1) {
|
||||
if (debug)
|
||||
fprintf(stderr, "+++++++++\n");
|
||||
// interpolate one sample
|
||||
*outptr++ = dithered_vol(((long)inptr[-2] + (long)inptr[0]) >> 1);
|
||||
*outptr++ = dithered_vol(((long)inptr[-1] + (long)inptr[1]) >> 1);
|
||||
} else if (stuff==-1) {
|
||||
if (debug)
|
||||
fprintf(stderr, "---------\n");
|
||||
inptr++;
|
||||
inptr++;
|
||||
}
|
||||
for (i=stuffsamp; i<frame_size + stuff; i++) {
|
||||
*outptr++ = dithered_vol(*inptr++);
|
||||
*outptr++ = dithered_vol(*inptr++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void *audio_thread_func(void *arg) {
|
||||
ao_device *dev = arg;
|
||||
int i, play_samples;
|
||||
|
||||
signed short buf_fill;
|
||||
signed short *inbuf, *outbuf;
|
||||
outbuf = malloc(OUTFRAME_BYTES);
|
||||
|
||||
#ifdef FANCY_RESAMPLING
|
||||
float *frame, *outframe;
|
||||
SRC_DATA srcdat;
|
||||
if (fancy_resampling) {
|
||||
frame = malloc(frame_size*2*sizeof(float));
|
||||
outframe = malloc(2*frame_size*2*sizeof(float));
|
||||
|
||||
srcdat.data_in = frame;
|
||||
srcdat.data_out = outframe;
|
||||
srcdat.input_frames = FRAME_BYTES;
|
||||
srcdat.output_frames = 2*FRAME_BYTES;
|
||||
srcdat.src_ratio = 1.0;
|
||||
srcdat.end_of_input = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
while (1) {
|
||||
do {
|
||||
inbuf = buffer_get_frame();
|
||||
} while (!inbuf);
|
||||
|
||||
#ifdef FANCY_RESAMPLING
|
||||
if (fancy_resampling) {
|
||||
for (i=0; i<2*FRAME_BYTES; i++) {
|
||||
frame[i] = (float)inbuf[i] / 32768.0;
|
||||
frame[i] *= volume;
|
||||
}
|
||||
srcdat.src_ratio = bf_playback_rate;
|
||||
src_process(src, &srcdat);
|
||||
assert(srcdat.input_frames_used == FRAME_BYTES);
|
||||
src_float_to_short_array(outframe, outbuf, FRAME_BYTES*2);
|
||||
play_samples = srcdat.output_frames_gen;
|
||||
} else
|
||||
#endif
|
||||
play_samples = stuff_buffer(bf_playback_rate, inbuf, outbuf);
|
||||
|
||||
ao_play(dev, (char *)outbuf, play_samples*4);
|
||||
}
|
||||
}
|
||||
|
||||
int init_output(void) {
|
||||
ao_initialize();
|
||||
int driver = ao_default_driver_id();
|
||||
|
||||
ao_sample_format fmt;
|
||||
fmt.bits = 16;
|
||||
fmt.rate = sampling_rate;
|
||||
fmt.channels = 2;
|
||||
fmt.byte_format = AO_FMT_LITTLE;
|
||||
fmt.matrix = 0;
|
||||
|
||||
ao_device *dev = ao_open_live(driver, &fmt, 0);
|
||||
|
||||
int err;
|
||||
#ifdef FANCY_RESAMPLING
|
||||
if (fancy_resampling)
|
||||
src = src_new(SRC_SINC_MEDIUM_QUALITY, 2, &err);
|
||||
else
|
||||
src = 0;
|
||||
#endif
|
||||
|
||||
pthread_t audio_thread;
|
||||
pthread_create(&audio_thread, NULL, audio_thread_func, dev);
|
||||
}
|
||||
Executable
+312
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
# Configure the following two options:
|
||||
# AP name - as will be shown in iTunes' menu
|
||||
# example:
|
||||
# my $apname = "SteePort";
|
||||
my $apname = "HairPort $$ on " . `hostname`;
|
||||
|
||||
# password - required to connect
|
||||
# for no password, set:
|
||||
# my $password = '';
|
||||
my $password = '';
|
||||
|
||||
# that's all!
|
||||
|
||||
# ShairPort - Airtunes compatible server
|
||||
# Copyright (c) 2011 James Laird
|
||||
# All rights reserved.
|
||||
# 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.
|
||||
|
||||
use IO::Select;
|
||||
use IO::Socket;
|
||||
use MIME::Base64;
|
||||
use HTTP::Request;
|
||||
use HTTP::Response;
|
||||
use IPC::Open2;
|
||||
use Crypt::OpenSSL::RSA;
|
||||
use Digest::MD5 qw/md5_hex/;
|
||||
use POSIX ":sys_wait_h";
|
||||
|
||||
|
||||
chomp $apname;
|
||||
|
||||
my @hw_addr = (0, map { int rand 256 } 1..5);
|
||||
|
||||
sub POPE {
|
||||
print "broken pipe\n";
|
||||
$SIG{PIPE} = \&POPE;
|
||||
}
|
||||
$SIG{PIPE} = \&POPE;
|
||||
|
||||
|
||||
my $avahi_publish = fork();
|
||||
if ($avahi_publish==0) {
|
||||
exec 'avahi-publish-service',
|
||||
join('', map { sprintf "%02X", $_ } @hw_addr) . "\@$apname",
|
||||
"_raop._tcp",
|
||||
"5000",
|
||||
"tp=UDP","sm=false","sv=false","ek=1","et=0,1","cn=0,1","ch=2","ss=16","sr=44100","pw=false","vn=3","txtvers=1";
|
||||
}
|
||||
|
||||
sub REAP {
|
||||
if ($avahi_publish == waitpid(-1, WNOHANG)) {
|
||||
die("Avahi publishing failed!");
|
||||
}
|
||||
printf("***CHILD EXITED***\n");
|
||||
$SIG{CHLD} = \&REAP;
|
||||
};
|
||||
$SIG{CHLD} = \&REAP;
|
||||
|
||||
my %conns;
|
||||
|
||||
$SIG{TERM} = $SIG{INT} = $SIG{__DIE__} = sub {
|
||||
map { eval { kill $_->{decoder_pid} } } keys %conns;
|
||||
kill 9, $avahi_publish;
|
||||
exit(0);
|
||||
};
|
||||
|
||||
my $airport_pem = join '', <DATA>;
|
||||
my $rsa = Crypt::OpenSSL::RSA->new_private_key($airport_pem) || die;
|
||||
|
||||
my $listen = new IO::Socket::INET(Listen => 1,
|
||||
LocalPort => 5000,
|
||||
ReuseAddr => 1,
|
||||
Proto => 'tcp');
|
||||
die "Can't listen on port 5000: $!" unless $listen;
|
||||
|
||||
my $sel = new IO::Select($listen);
|
||||
|
||||
print "listening...\n";
|
||||
|
||||
while (1) {
|
||||
my @waiting = $sel->can_read;
|
||||
foreach $fh (@waiting) {
|
||||
if ($fh==$listen) {
|
||||
my $new = $listen->accept;
|
||||
print "new connection\n";
|
||||
$sel->add($new);
|
||||
$new->blocking(0);
|
||||
$conns{$new} = {fh => $fh};
|
||||
} else {
|
||||
if (eof($fh)) {
|
||||
print "closed: $fh\n";
|
||||
$sel->remove($fh);
|
||||
close $fh;
|
||||
eval { kill $conns{$fh}{decoder_pid} };
|
||||
delete $conns{$fh};
|
||||
next;
|
||||
}
|
||||
if (exists $conns{$fh}) {
|
||||
conn_handle_data($fh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub conn_handle_data {
|
||||
my $fh = shift;
|
||||
my $conn = $conns{$fh};
|
||||
|
||||
if ($conn->{req_need}) {
|
||||
if (length($conn->{data}) >= $conn->{req_need}) {
|
||||
$conn->{req}->content(substr($conn->{data}, 0, $conn->{req_need}, ''));
|
||||
conn_handle_request($fh, $conn);
|
||||
}
|
||||
undef $conn->{req_need};
|
||||
return;
|
||||
}
|
||||
|
||||
read $fh, my $data, 4096;
|
||||
$conn->{data} .= $data;
|
||||
|
||||
if ($conn->{data} =~ /(\r\n\r\n|\n\n|\r\r)/) {
|
||||
my $req_data = substr($conn->{data}, 0, $+[0], '');
|
||||
$conn->{req} = HTTP::Request->parse($req_data);
|
||||
printf "REQ: %s\n", $conn->{req}->method;
|
||||
conn_handle_request($fh, $conn);
|
||||
conn_handle_data($fh) if length($conn->{data});
|
||||
}
|
||||
}
|
||||
|
||||
sub digest_ok {
|
||||
my ($req, $conn) = @_;
|
||||
my $authz = $req->header('Authorization');
|
||||
return 0 unless $authz =~ s/^Digest\s+//i;
|
||||
return 0 unless length $conn->{nonce};
|
||||
my @authz = split /,\s*/, $authz;
|
||||
my %authz = map { /(.+)="(.+)"/; ($1, $2) } @authz;
|
||||
|
||||
# not a standard digest - uses capital hex digits, in conflict with the RFC
|
||||
my $digest = uc md5_hex (
|
||||
uc(md5_hex($authz{username} . ':' . $authz{realm} . ':' . $password))
|
||||
. ':' . $authz{nonce} . ':' .
|
||||
uc(md5_hex($req->method . ':' . $authz{uri}))
|
||||
);
|
||||
|
||||
return $digest eq $authz{response};
|
||||
}
|
||||
|
||||
sub conn_handle_request {
|
||||
my ($fh, $conn) = @_;
|
||||
|
||||
my $req = $conn->{req};;
|
||||
my $clen = $req->header('content-length') // 0;
|
||||
if ($clen > 0 && !length($req->content)) {
|
||||
$conn->{req_need} = $clen;
|
||||
return; # need more!
|
||||
}
|
||||
|
||||
my $resp = HTTP::Response->new(200);
|
||||
$resp->request($req);
|
||||
$resp->protocol($req->protocol);
|
||||
|
||||
$resp->header('CSeq', $req->header('CSeq'));
|
||||
$resp->header('Audio-Jack-Status', 'connected; type=analog');
|
||||
|
||||
if (my $chall = $req->header('Apple-Challenge')) {
|
||||
my $data = decode_base64($chall);
|
||||
$data .= join '', map { chr } split(/\./, $fh->sockhost);
|
||||
$data .= join '', map { chr } @hw_addr;
|
||||
$data .= chr(0) x (0x20-length($data));
|
||||
|
||||
$rsa->use_pkcs1_padding; # this isn't hashed before signing
|
||||
my $signature = encode_base64 $rsa->private_encrypt($data), '';
|
||||
$signature =~ s/=*$//;
|
||||
$resp->header('Apple-Response', $signature);
|
||||
}
|
||||
|
||||
if (length $password) {
|
||||
if (!digest_ok($req, $conn)) {
|
||||
my $nonce = md5_hex(map { rand } 1..20);
|
||||
$conn->{nonce} = $nonce;
|
||||
$resp->header('WWW-Authenticate', "Digest realm=\"$apname\", nonce=\"$nonce\"");
|
||||
$resp->code(401);
|
||||
$req->method('DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
for ($req->method) {
|
||||
/^OPTIONS$/ && do {
|
||||
$resp->header('Public', 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER');
|
||||
last;
|
||||
};
|
||||
|
||||
/^ANNOUNCE$/ && do {
|
||||
my $sdptext = $req->content;
|
||||
my @sdplines = split /[\r\n]+/, $sdptext;
|
||||
my %sdp = map { ($1, $2) if /^a=([^:]+):(.+)/ } @sdplines;
|
||||
die("no AESIV") unless my $aesiv = decode_base64($sdp{aesiv});
|
||||
die("no AESKEY") unless my $rsaaeskey = decode_base64($sdp{rsaaeskey});
|
||||
$rsa->use_pkcs1_oaep_padding;
|
||||
my $aeskey = $rsa->decrypt($rsaaeskey) || die;
|
||||
|
||||
$conn->{aesiv} = $aesiv;
|
||||
$conn->{aeskey} = $aeskey;
|
||||
$conn->{fmtp} = $sdp{fmtp};
|
||||
last;
|
||||
};
|
||||
|
||||
/^SETUP$/ && do {
|
||||
my $transport = $req->header('Transport');
|
||||
$transport =~ s/;control_port=(\d+)//;
|
||||
my $cport = $1;
|
||||
$transport =~ s/;timing_port=(\d+)//;
|
||||
my $tport = $1;
|
||||
$transport =~ s/;server_port=(\d+)//;
|
||||
my $dport = $1;
|
||||
$resp->header('Session', 'DEADBEEF');
|
||||
|
||||
|
||||
my $dec = sprintf("./hairtunes iv %s key %s fmtp %s cport %s tport %s dport %s host %s",
|
||||
map { "'$_'" } (
|
||||
unpack('H*', $conn->{aesiv}),
|
||||
unpack('H*', $conn->{aeskey}),
|
||||
$conn->{fmtp},
|
||||
$cport, $tport, $dport,
|
||||
'unused'
|
||||
));
|
||||
# print "decode command: $dec\n";
|
||||
my $decoder = open2(my $dec_out, my $dec_in, $dec);
|
||||
|
||||
$conn->{decoder_pid} = $decoder;
|
||||
$conn->{decoder_fh} = $dec_in;
|
||||
my $portdesc = <$dec_out>;
|
||||
die("Expected port number from decoder; got $portdesc") unless $portdesc =~ /^port: (\d+)/;
|
||||
my $port = $1;
|
||||
print "launched decoder: $decoder on port: $port\n";
|
||||
$resp->header('Transport', $req->header('Transport') . ";server_port=$port");
|
||||
last;
|
||||
};
|
||||
|
||||
/^RECORD$/ && last;
|
||||
/^FLUSH$/ && do {
|
||||
my $dfh = $conn->{decoder_fh};
|
||||
print $dfh "flush\n";
|
||||
last;
|
||||
};
|
||||
/^TEARDOWN$/ && do {
|
||||
$resp->header('Connection', 'close');
|
||||
close $conn->{decoder_fh};
|
||||
last;
|
||||
};
|
||||
/^SET_PARAMETER$/ && do {
|
||||
my @lines = split /[\r\n]+/, $req->content;
|
||||
my %content = map { /^(\S+): (.+)/; (lc $1, $2) } @lines;
|
||||
my $cfh = $conn->{decoder_fh};
|
||||
if (exists $content{volume}) {
|
||||
printf $cfh "vol: %f\n", $content{volume};
|
||||
}
|
||||
last;
|
||||
};
|
||||
/^GET_PARAMETER$/ && last;
|
||||
/^DENIED$/ && last;
|
||||
die("Unknown method: $_");
|
||||
}
|
||||
|
||||
print $fh $resp->as_string("\r\n");
|
||||
$fh->flush;
|
||||
}
|
||||
|
||||
__DATA__
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt
|
||||
wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U
|
||||
wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf
|
||||
/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/
|
||||
UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW
|
||||
BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa
|
||||
LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5
|
||||
NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm
|
||||
lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz
|
||||
aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu
|
||||
a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM
|
||||
oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z
|
||||
oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+
|
||||
k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL
|
||||
AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA
|
||||
cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf
|
||||
54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov
|
||||
17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc
|
||||
1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI
|
||||
LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ
|
||||
2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKaXTyY=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user